Giter Club home page Giter Club logo

laravel-adjacency-list's Introduction

Laravel Adjacency List

CI Code Coverage Scrutinizer Code Quality Latest Stable Version Total Downloads License

This Laravel Eloquent extension provides recursive relationships for trees and graphs using common table expressions (CTE).

Compatibility

  • MySQL 8.0+
  • MariaDB 10.2+
  • PostgreSQL 9.4+
  • SQLite 3.8.3+
  • SQL Server 2008+
  • SingleStore 8.1+ (only trees)
  • Firebird

Installation

composer require staudenmeir/laravel-adjacency-list:"^1.0"

Use this command if you are in PowerShell on Windows (e.g. in VS Code):

composer require staudenmeir/laravel-adjacency-list:"^^^^1.0"

Versions

Laravel Package
11.x 1.21
10.x 1.13
9.x 1.12
8.x 1.9
7.x 1.5
6.x 1.3
5.8 1.1
5.5–5.7 1.0

Usage

The package offers recursive relationships for traversing two types of data structures:

Trees: One Parent per Node (One-to-Many)

Use the package to traverse a tree structure with one parent per node. Use cases might be recursive categories, a page hierarchy or nested comments.

Supports Laravel 5.5+.

Getting Started

Consider the following table schema for hierarchical data in trees:

Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->unsignedBigInteger('parent_id')->nullable();
});

Use the HasRecursiveRelationships trait in your model to work with recursive relationships:

class User extends Model
{
    use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;
}

By default, the trait expects a parent key named parent_id. You can customize it by overriding getParentKeyName():

class User extends Model
{
    use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;
    
    public function getParentKeyName()
    {
        return 'parent_id';
    }
}

By default, the trait uses the model's primary key as the local key. You can customize it by overriding getLocalKeyName():

class User extends Model
{
    use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;
    
    public function getLocalKeyName()
    {
        return 'id';
    }
}

Included Relationships

The trait provides various relationships:

  • ancestors(): The model's recursive parents.
  • ancestorsAndSelf(): The model's recursive parents and itself.
  • bloodline(): The model's ancestors, descendants and itself.
  • children(): The model's direct children.
  • childrenAndSelf(): The model's direct children and itself.
  • descendants(): The model's recursive children.
  • descendantsAndSelf(): The model's recursive children and itself.
  • parent(): The model's direct parent.
  • parentAndSelf(): The model's direct parent and itself.
  • rootAncestor(): The model's topmost parent.
  • rootAncestorOrSelf(): The model's topmost parent or itself.
  • siblings(): The parent's other children.
  • siblingsAndSelf(): All the parent's children.
$ancestors = User::find($id)->ancestors;

$users = User::with('descendants')->get();

$users = User::whereHas('siblings', function ($query) {
    $query->where('name', 'John');
})->get();

$total = User::find($id)->descendants()->count();

User::find($id)->descendants()->update(['active' => false]);

User::find($id)->siblings()->delete();

Trees

The trait provides the tree() query scope to get all models, beginning at the root(s):

$tree = User::tree()->get();

treeOf() allows you to query trees with custom constraints for the root model(s). Consider a table with multiple separate lists:

$constraint = function ($query) {
    $query->whereNull('parent_id')->where('list_id', 1);
};

$tree = User::treeOf($constraint)->get();

You can also pass a maximum depth:

$tree = User::tree(3)->get();

$tree = User::treeOf($constraint, 3)->get();

Filters

The trait provides query scopes to filter models by their position in the tree:

  • hasChildren(): Models with children.
  • hasParent(): Models with a parent.
  • isLeaf()/doesntHaveChildren(): Models without children.
  • isRoot(): Models without a parent.
$noLeaves = User::hasChildren()->get();

$noRoots = User::hasParent()->get();

$leaves = User::isLeaf()->get();
$leaves = User::doesntHaveChildren()->get();

$roots = User::isRoot()->get();

Order

The trait provides query scopes to order models breadth-first or depth-first:

  • breadthFirst(): Get siblings before children.
  • depthFirst(): Get children before siblings.
$tree = User::tree()->breadthFirst()->get();

$descendants = User::find($id)->descendants()->depthFirst()->get();

Depth

The results of ancestor, bloodline, descendant and tree queries include an additional depth column.

It contains the model's depth relative to the query's parent. The depth is positive for descendants and negative for ancestors:

$descendantsAndSelf = User::find($id)->descendantsAndSelf()->depthFirst()->get();

echo $descendantsAndSelf[0]->depth; // 0
echo $descendantsAndSelf[1]->depth; // 1
echo $descendantsAndSelf[2]->depth; // 2

You can customize the column name by overriding getDepthName():

class User extends Model
{
    use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;

    public function getDepthName()
    {
        return 'depth';
    }
}
Depth Constraints

You can use the whereDepth() query scope to filter models by their relative depth:

$descendants = User::find($id)->descendants()->whereDepth(2)->get();

$descendants = User::find($id)->descendants()->whereDepth('<', 3)->get();

Queries with whereDepth() constraints that limit the maximum depth still build the entire (sub)tree internally. Use withMaxDepth() to set a maximum depth that improves query performance by only building the requested section of the tree:

$descendants = User::withMaxDepth(3, function () use ($id) {
    return User::find($id)->descendants;
});

This also works with negative depths (where it's technically a minimum):

$ancestors = User::withMaxDepth(-3, function () use ($id) {
    return User::find($id)->ancestors;
});

Path

The results of ancestor, bloodline, descendant and tree queries include an additional path column.

It contains the dot-separated path of local keys from the query's parent to the model:

$descendantsAndSelf = User::find(1)->descendantsAndSelf()->depthFirst()->get();

echo $descendantsAndSelf[0]->path; // 1
echo $descendantsAndSelf[1]->path; // 1.2
echo $descendantsAndSelf[2]->path; // 1.2.3

You can customize the column name and the separator by overriding the respective methods:

class User extends Model
{
    use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;

    public function getPathName()
    {
        return 'path';
    }

    public function getPathSeparator()
    {
        return '.';
    }
}

Custom Paths

You can add custom path columns to the query results:

class User extends Model
{
    use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;

    public function getCustomPaths()
    {
        return [
            [
                'name' => 'slug_path',
                'column' => 'slug',
                'separator' => '/',
            ],
        ];
    }
}

$descendantsAndSelf = User::find(1)->descendantsAndSelf;

echo $descendantsAndSelf[0]->slug_path; // user-1
echo $descendantsAndSelf[1]->slug_path; // user-1/user-2
echo $descendantsAndSelf[2]->slug_path; // user-1/user-2/user-3

You can also reverse custom paths:

class User extends Model
{
    use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;

    public function getCustomPaths()
    {
        return [
            [
                'name' => 'reverse_slug_path',
                'column' => 'slug',
                'separator' => '/',
                'reverse' => true,
            ],
        ];
    }
}

Nested Results

Use the toTree() method on a result collection to generate a nested tree:

$users = User::tree()->get();

$tree = $users->toTree();

This recursively sets children relationships:

[
  {
    "id": 1,
    "children": [
      {
        "id": 2,
        "children": [
          {
            "id": 3,
            "children": []
          }
        ]
      },
      {
        "id": 4,
        "children": [
          {
            "id": 5,
            "children": []
          }
        ]
      }
    ]
  }
]

Initial & Recursive Query Constraints

You can add custom constraints to the CTE's initial and recursive query. Consider a query where you want to traverse a tree while skipping inactive users and their descendants:

$tree = User::withQueryConstraint(function (Builder $query) {
   $query->where('users.active', true);
}, function () {
   return User::tree()->get();
});

You can also add a custom constraint to only the initial or recursive query using withInitialQueryConstraint()/ withRecursiveQueryConstraint().

Additional Methods

The trait also provides methods to check relationships between models:

  • isChildOf(Model $model): Checks if the current model is a child of the given model.
  • isParentOf(Model $model): Checks if the current model is a parent of the given model.
  • getDepthRelatedTo(Model $model): Returns the depth of the current model related to the given model.
$rootUser = User::create(['parent_id' => null]); 
$firstLevelUser = User::create(['parent_id' => $rootUser->id]); 
$secondLevelUser = User::create(['parent_id' => $firstLevelUser->id]);  

$isChildOf = $secondLevelUser->isChildOf($firstLevelUser); // Output: true
$isParentOf = $rootUser->isParentOf($firstLevelUser); // Output: true
$depthRelatedTo = $secondLevelUser->getDepthRelatedTo($rootUser); // Output: 2

Custom Relationships

You can also define custom relationships to retrieve related models recursively.

HasManyOfDescendants

Consider a HasMany relationship between User and Post:

class User extends Model
{
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}

Define a HasManyOfDescendants relationship to get all posts of a user and its descendants:

class User extends Model
{
    use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;

    public function recursivePosts()
    {
        return $this->hasManyOfDescendantsAndSelf(Post::class);
    }
}

$recursivePosts = User::find($id)->recursivePosts;

$users = User::withCount('recursivePosts')->get();

Use hasManyOfDescendants() to only get the descendants' posts:

class User extends Model
{
    use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;

    public function descendantPosts()
    {
        return $this->hasManyOfDescendants(Post::class);
    }
}
BelongsToManyOfDescendants

Consider a BelongsToMany relationship between User and Role:

class User extends Model
{
    public function roles()
    {
        return $this->belongsToMany(Role::class);
    }
}

Define a BelongsToManyOfDescendants relationship to get all roles of a user and its descendants:

class User extends Model
{
    use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;

    public function recursiveRoles()
    {
        return $this->belongsToManyOfDescendantsAndSelf(Role::class);
    }
}

$recursiveRoles = User::find($id)->recursiveRoles;

$users = User::withCount('recursiveRoles')->get();

Use belongsToManyOfDescendants() to only get the descendants' roles:

class User extends Model
{
    use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;

    public function descendantRoles()
    {
        return $this->belongsToManyOfDescendants(Role::class);
    }
}
MorphToManyOfDescendants

Consider a MorphToMany relationship between User and Tag:

class User extends Model
{
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}

Define a MorphToManyOfDescendants relationship to get all tags of a user and its descendants:

class User extends Model
{
    use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;

    public function recursiveTags()
    {
        return $this->morphToManyOfDescendantsAndSelf(Tag::class, 'taggable');
    }
}

$recursiveTags = User::find($id)->recursiveTags;

$users = User::withCount('recursiveTags')->get();

Use morphToManyOfDescendants() to only get the descendants' tags:

class User extends Model
{
    use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;

    public function descendantTags()
    {
        return $this->morphToManyOfDescendants(Tag::class, 'taggable');
    }
}
MorphedByManyOfDescendants

Consider a MorphedByMany relationship between Category and Post:

class Category extends Model
{
    public function posts()
    {
        return $this->morphedByMany(Post::class, 'categorizable');
    }
}

Define a MorphedByManyOfDescendants relationship to get all posts of a category and its descendants:

class Category extends Model
{
    use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;

    public function recursivePosts()
    {
        return $this->morphedByManyOfDescendantsAndSelf(Post::class, 'categorizable');
    }
}

$recursivePosts = Category::find($id)->recursivePosts;

$categories = Category::withCount('recursivePosts')->get();

Use morphedByManyOfDescendants() to only get the descendants' posts:

class Category extends Model
{
    use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;

    public function descendantPosts()
    {
        return $this->morphedByManyOfDescendants(Post::class, 'categorizable');
    }
}
Intermediate Scopes

You can adjust the descendants query (e.g. child users) by adding or removing intermediate scopes:

User::find($id)->recursivePosts()->withTrashedDescendants()->get();

User::find($id)->recursivePosts()->withIntermediateScope('active', new ActiveScope())->get();

User::find($id)->recursivePosts()->withIntermediateScope(
    'depth',
    function ($query) {
        $query->whereDepth('<=', 10);
    }
)->get();

User::find($id)->recursivePosts()->withoutIntermediateScope('active')->get();
Usage outside of Laravel

If you are using the package outside of Laravel or have disabled package discovery for staudenmeir/laravel-cte, you need to add support for common table expressions to the related model:

class Post extends Model
{
    use \Staudenmeir\LaravelCte\Eloquent\QueriesExpressions;
}

Deep Relationship Concatenation

You can include recursive relationships into deep relationships by concatenating them with other relationships using staudenmeir/eloquent-has-many-deep. This works with Ancestors, Bloodline and Descendants relationships (Laravel 9+).

Consider a HasMany relationship between User and Post and building a deep relationship to get all posts of a user's descendants:

User β†’ descendants β†’ User β†’ has many β†’ Post

Install the additional package, add the HasRelationships trait to the recursive model and define a deep relationship:

class User extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;
    use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;

    public function descendantPosts()
    {
        return $this->hasManyDeepFromRelations(
            $this->descendants(),
            (new static)->posts()
        );
    }
    
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}

$descendantPosts = User::find($id)->descendantPosts;

At the moment, recursive relationships can only be at the beginning of deep relationships:

  • Supported: User β†’ descendants β†’ User β†’ has many β†’ Post
  • Not supported: Post β†’ belongs to β†’ User β†’ descendants β†’ User

Known Issues

MariaDB doesn't yet support correlated CTEs in subqueries. This affects queries like User::whereHas('descendants') or User::withCount('descendants').

Graphs: Multiple Parents per Node (Many-to-Many)

You can also use the package to traverse graphs with multiple parents per node that are defined in a pivot table. Use cases might be a bill of materials (BOM) or a family tree.

Supports Laravel 9+.

Consider the following table schema for storing directed graphs as nodes and edges:

Schema::create('nodes', function (Blueprint $table) {
    $table->id();
});

Schema::create('edges', function (Blueprint $table) {
    $table->unsignedBigInteger('source_id');
    $table->unsignedBigInteger('target_id');
    $table->string('label');
    $table->unsignedBigInteger('weight');
});

Use the HasGraphRelationships trait in your model to work with graph relationships and specify the name of the pivot table:

class Node extends Model
{
    use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasGraphRelationships;

    public function getPivotTableName(): string
    {
        return 'edges';
    }
}

By default, the trait expects a parent key named parent_id and child key named child_id in the pivot table. You can customize them by overriding getParentKeyName() and getChildKeyName():

class Node extends Model
{
    public function getParentKeyName(): string
    {
        return 'source_id';
    }
  
    public function getChildKeyName(): string
    {
        return 'target_id';
    }
}

By default, the trait uses the model's primary key as the local key. You can customize it by overriding getLocalKeyName():

class Node extends Model
{
    public function getLocalKeyName(): string
    {
        return 'id';
    }
}

The trait provides various relationships:

  • ancestors(): The node's recursive parents.
  • ancestorsAndSelf(): The node's recursive parents and itself.
  • children(): The node's direct children.
  • childrenAndSelf(): The node's direct children and itself.
  • descendants(): The node's recursive children.
  • descendantsAndSelf(): The node's recursive children and itself.
  • parents(): The node's direct parents.
  • parentsAndSelf(): The node's direct parents and itself.
$ancestors = Node::find($id)->ancestors;

$nodes = Node::with('descendants')->get();

$nodes = Node::has('children')->get();

$total = Node::find($id)->descendants()->count();

Node::find($id)->descendants()->update(['active' => false]);

Node::find($id)->parents()->delete();

Similar to BelongsToMany relationships, you can retrieve additional columns from the pivot table besides the parent and child key:

class Node extends Model
{
    public function getPivotColumns(): array
    {
        return ['label', 'weight'];
    }
}

$nodes = Node::find($id)->descendants;

foreach ($nodes as $node) {
    dump(
        $node->pivot->label,
        $node->pivot->weight
    );
}

If your graph contains cycles, you need to enable cycle detection to prevent infinite loops:

class Node extends Model
{
    public function enableCycleDetection(): bool
    {
        return true;
    }
}

You can also retrieve the start of a cycle, i.e. the first duplicate node. With this option, the query results include an is_cycle column that indicates whether the node is part of a cycle:

class Node extends Model
{
    public function enableCycleDetection(): bool
    {
        return true;
    }

    public function includeCycleStart(): bool
    {
        return true;
    }
}

$nodes = Node::find($id)->descendants;

foreach ($nodes as $node) {
    dump($node->is_cycle);
}

The trait provides the subgraph() query scope to get the subgraph of a custom constraint:

$constraint = function ($query) {
    $query->whereIn('id', $ids);
};

$subgraph = Node::subgraph($constraint)->get();

You can pass a maximum depth as the second argument:

$subgraph = Node::subgraph($constraint, 3)->get();

The trait provides query scopes to order nodes breadth-first or depth-first:

  • breadthFirst(): Get siblings before children.
  • depthFirst(): Get children before siblings.
$descendants = Node::find($id)->descendants()->breadthFirst()->get();

$descendants = Node::find($id)->descendants()->depthFirst()->get();

The results of ancestor, descendant and subgraph queries include an additional depth column.

It contains the node's depth relative to the query's parent. The depth is positive for descendants and negative for ancestors:

$descendantsAndSelf = Node::find($id)->descendantsAndSelf()->depthFirst()->get();

echo $descendantsAndSelf[0]->depth; // 0
echo $descendantsAndSelf[1]->depth; // 1
echo $descendantsAndSelf[2]->depth; // 2

You can customize the column name by overriding getDepthName():

class Node extends Model
{
    public function getDepthName(): string
    {
        return 'depth';
    }
}
Depth Constraints

You can use the whereDepth() query scope to filter nodes by their relative depth:

$descendants = Node::find($id)->descendants()->whereDepth(2)->get();

$descendants = Node::find($id)->descendants()->whereDepth('<', 3)->get();

Queries with whereDepth() constraints that limit the maximum depth still build the entire (sub)graph internally. Use withMaxDepth() to set a maximum depth that improves query performance by only building the requested section of the graph:

$descendants = Node::withMaxDepth(3, function () use ($id) {
    return Node::find($id)->descendants;
});

This also works with negative depths (where it's technically a minimum):

$ancestors = Node::withMaxDepth(-3, function () use ($id) {
    return Node::find($id)->ancestors;
});

The results of ancestor, descendant and subgraph queries include an additional path column.

It contains the dot-separated path of local keys from the query's parent to the node:

$descendantsAndSelf = Node::find(1)->descendantsAndSelf()->depthFirst()->get();

echo $descendantsAndSelf[0]->path; // 1
echo $descendantsAndSelf[1]->path; // 1.2
echo $descendantsAndSelf[2]->path; // 1.2.3

You can customize the column name and the separator by overriding the respective methods:

class Node extends Model
{
    public function getPathName(): string
    {
        return 'path';
    }

    public function getPathSeparator(): string
    {
        return '.';
    }
}

You can add custom path columns to the query results:

class Node extends Model
{
    public function getCustomPaths(): array
    {
        return [
            [
                'name' => 'slug_path',
                'column' => 'slug',
                'separator' => '/',
            ],
        ];
    }
}

$descendantsAndSelf = Node::find(1)->descendantsAndSelf;

echo $descendantsAndSelf[0]->slug_path; // node-1
echo $descendantsAndSelf[1]->slug_path; // node-1/node-2
echo $descendantsAndSelf[2]->slug_path; // node-1/node-2/node-3

You can also reverse custom paths:

class Node extends Model
{
    public function getCustomPaths(): array
    {
        return [
            [
                'name' => 'reverse_slug_path',
                'column' => 'slug',
                'separator' => '/',
                'reverse' => true,
            ],
        ];
    }
}

Use the toTree() method on a result collection to generate a nested tree:

$nodes = Node::find($id)->descendants;

$tree = $nodes->toTree();

This recursively sets children relationships:

[
  {
    "id": 1,
    "children": [
      {
        "id": 2,
        "children": [
          {
            "id": 3,
            "children": []
          }
        ]
      },
      {
        "id": 4,
        "children": [
          {
            "id": 5,
            "children": []
          }
        ]
      }
    ]
  }
]

You can add custom constraints to the CTE's initial and recursive query. Consider a query where you want to traverse a node's descendants while skipping inactive nodes and their descendants:

$descendants = Node::withQueryConstraint(function (Builder $query) {
   $query->where('nodes.active', true);
}, function () {
   return Node::find($id)->descendants;
});

You can also add a custom constraint to only the initial or recursive query using withInitialQueryConstraint()/ withRecursiveQueryConstraint().

You can include recursive relationships into deep relationships by concatenating them with other relationships using staudenmeir/eloquent-has-many-deep (Laravel 9+).

Consider a HasMany relationship between Node and Post and building a deep relationship to get all posts of a node's descendants:

Node β†’ descendants β†’ Node β†’ has many β†’ Post

Install the additional package, add the HasRelationships trait to the recursive model and define a deep relationship:

class Node extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;
    use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;

    public function descendantPosts()
    {
        return $this->hasManyDeepFromRelations(
            $this->descendants(),
            (new static)->posts()
        );
    }
    
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}

$descendantPosts = Node::find($id)->descendantPosts;

At the moment, recursive relationships can only be at the beginning of deep relationships:

  • Supported: Node β†’ descendants β†’ Node β†’ has many β†’ Post
  • Not supported: Post β†’ belongs to β†’ Node β†’ descendants β†’ Node

MariaDB doesn't yet support correlated CTEs in subqueries. This affects queries like Node::whereHas('descendants') or Node::withCount('descendants').

Package Conflicts

Contributing

Please see CONTRIBUTING and CODE OF CONDUCT for details.

laravel-adjacency-list's People

Contributors

boumanb avatar calebdw avatar decadence avatar l3aro avatar loribean avatar nandi95 avatar paperturtle avatar philipboeken avatar propaganistas avatar raustin-m avatar staudenmeir avatar vanodevium avatar whoami15 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

laravel-adjacency-list's Issues

Dealing with SoftDeletes models

Hi there! First of all, thanks a lot for this great piece of code!

I'm having such a hard time dealing with SoftDeletes.

The following will work just as expected:

// At AssetController@index($id)
$asset = Asset::with('user', 'roles')->findOrFail($id);
$ancestors = $asset->ancestorsAndSelf->reverse();

This also works great:

// At AssetController@delete($id)
$asset = Asset::find($id);
$children = $asset->descendantsAndSelf->reverse();

foreach ($children as $child) {
    $child->delete();
}

But then I need to handle soft deleted stuff. For instance:

// At TrashController@index($id)
$asset = Asset::onlyTrashed()->findOrFail($id);
$ancestors = $asset->ancestorsAndSelf->reverse();

Or to restore the once deleted tree:

// At TrashController@destroy($id)
$asset = Asset::onlyTrashed()->find($id);
$children = $asset->descendantsAndSelf;

foreach ($children as $child) {
    $child->restore();
}

All onlyTrashed() queries will produce:

SELECT * FROM `laravel_cte` WHERE `laravel_cte`.`deleted_at` IS NULL

but to get soft deleted stuff we need:

SELECT * FROM `laravel_cte` WHERE `laravel_cte`.`deleted_at` IS NOT NULL

Is there a way I could fix the above in the application level or does it require a package enhancement?

Thanks in advance!

Recursive update failed - sql syntax error

This causes SQL syntax error:

// Node is laravel model
Node::findOrFail($id)->descendantsAndSelf()->update(['trashed_at'=>null]);

the generated SQL:

WITH recursive `laravel_cte` AS (
    (
        SELECT *,
               0                         AS `depth`,
               cast(`id` AS char(65535)) AS `path`
        FROM   `nodes`
        WHERE  `nodes`.`id` = 2)
    UNION ALL
    (
        SELECT     `nodes`.*,
                   `depth` + 1 AS `depth`,
                   concat(`path`, '.', `nodes`.`id`)
        FROM       `nodes`
                       INNER JOIN `laravel_cte`
                                  ON         `laravel_cte`.`id` = `nodes`.`parent_id`
        )
)
UPDATE `nodes`
SET    `trashed_at` = NULL
WHERE  `nodes`.`id` IN
       (
           SELECT `id`
           FROM   `laravel_cte`
       )

Error

ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'update `nodes` set `trashed_at` = null where `nodes`.`id` in (select `id` from `' at line 1

nodes table structure:

id 
parent_id
trashed_at

software versions:

PHP version: 7.2.22
Database Server version: 10.2.25-MariaDB-log - Homebrew
Laravel: 6.0


Thanks

Getting tree via another model relationship

Hi,

Brilliant plugin! I'm having a bit of an issue where the resulting query generates an SQL error.

I have two models - User and File with corresponding tables, joined by a FileUser pivot model.

The File model has a parent_id and id and uses the HasRecursiveRelationships trait. So what I'm actually trying to do is grab files that are recursively owned by the user -

// get files recursively owned by the user
auth()->user()->files()->tree()->get();

// even better would be to further filter this tree
auth()->user()->files()->tree()->where('name', 'LIKE', '%testing%')->get();

However, when running either of the above the resulting query is -

with recursive `laravel_cte` as (
    (
        select *, 0 as `depth`, cast(`id` as char(65535)) as `path`
        from `files`

        where `parent_id` is null)

    union all
    (
        select `files`.*,
               `depth` + 1 as `depth`,
               concat(`path`, '.', `files`.`id`)
        from `files`
                 inner join `laravel_cte` on `laravel_cte`.`id` = `files`.`parent_id`
    )
)
select `laravel_cte`.*,
       `file_user`.`user_id` as `pivot_user_id`,
       `file_user`.`file_id` as `pivot_file_id`
from `laravel_cte`
         inner join `file_user` on `files`.`id` = `file_user`.`file_id`

where `file_user`.`user_id` = '1'
  and `name` LIKE '%testing%'
  and `laravel_cte`.`deleted_at` is null
order by `name` asc

and an error is generated because of the join on line 21

inner join `file_user` on `files`.`id` = `file_user`.`file_id`

At this point in the query, it doesn't actually have access to the files table. Is querying in this way just not at all possible, or is this a slight bug in the plugin?

Thanks,
Chris.

[Feature Request] Customisable path key name

Sorry, me again!

It doesn't appear to be possible currently, but I'm wondering if it would be possible to add an additional customisation to the generated path field, specifically a way to set the key name to be used in the path.

My use case is to generate slug-based URLs which include all ancestors' slugs, for example slug-one/slug-two/slug-three. I am currently doing this after the database query has been run, but this increases the number of hydrated models quite quickly.

I imagine it would require a new method which would look something like this:

class Page extends Model
{
    use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;

    public function getPathKeyName()
    {
        return 'slug';    // This would default to 'id' if not set
    }

    public function getPathSeparator()
    {
        return '/';    // To create the URL from the example above
    }
}

Is this possible to do without breaking the current functionality? I'd be happy to try put together a PR for it if you'd like, but my knowledge of CTEs is quite limited!

Thank you!

Getting Descendant of Ancestor with Descendant in a single query

Hello,
Here you can see my ancestor and descendant table,
Ancestor | Descendant
Electronics | Computer
Electronics | Laptop
Electronics | Mobile phone

Here is my code,
$descendants = Category::find($id)->descendants()->whereDepth(1)->get();
foreach ($descendants as $value) {
echo $value->name;
$ancestor=Category::find($value->id)->ancestors;
echo $ancestor[0]->name;
}
}

Question:
As of right now, this works fine, but there will be a lot of queries if I handle some large amount of data, so is there any way to get the descendant of the ancestor with a descendant from a single query?

Thanks

Control maximum depth inside the recursive query

Problem

When limiting the depth with whereDepth(), the CTE will still first build the entire tree which can lead to a big performance hit.

Thoughts

I feel it would be better to be able to break out of the recursive query.

It can be done by moving the where clause into the recursive query:

union all (
   select ...
   inner join "laravel_cte" on "laravel_cte"."id" = "nodes"."parent_id"1
   where "depth" + 1 < MAX_DEPTH
)

We would have to change the api, for example:

public function scopeTree(Builder $query, int $maxDepth=null)

Perspective

More generally, I think we should be able to manipulate both the initialQuery and the recursiveQuery so we can apply conditions on them and limit performance issues.

Conflict with illuminate/database dependency

Laravel Framework v5.8.30.

I can't install 1.3.1 version it requires illuminate/database ^6.0.

See - staudenmeir/laravel-adjacency-list v1.3.1 requires illuminate/database ^6.0 -> satisfiable by illuminate/database[6.x-dev, v6.0.0, v6.0.1, v6.0.2, v6.0.3, v6.0.4, v6.1.0, v6.10.0, v6.11.0, v6.12.0, v6.13.0, v6.13.1, v6.14.0, v6.15.0, v6.15.1, v6.16.0, v6.17.0, v6.17.1, v6.18.0, v6.18.1, v6.18.10, v6.18.11, v6.18.2, v6.18.3, v6.18.4, v6.18.5, v6.18.6, v6.18.7, v6.18.8, v6.18.9, v6.2.0, v6.3.0, v6.4.1, v6.5.0, v6.5.1, v6.5.2, v6.6.0, v6.6.1, v6.6.2, v6.7.0, v6.8.0].

I can only install the version 1.1.4

[Feature request] Recursive relationships defined in related table

It would be super awesome if the recursive relationships could be defined in a related table, so a model could have multiple parents.

the nodes table:

+-------+------------------+------+-----+---------+----------------+
| Field | Type             | Null | Key | Default | Extra          |
+-------+------------------+------+-----+---------+----------------+
| id    | int(11) unsigned | NO   | PRI | NULL    | auto_increment |
| name  | varchar(100)     | NO   |     | NULL    |                |
+-------+------------------+------+-----+---------+----------------+

parent/child relationship table:

+-----------+------------------+------+-----+---------+-------+
| Field     | Type             | Null | Key | Default | Extra |
+-----------+------------------+------+-----+---------+-------+
| id        | int(11)          | NO   |     | NULL    |       |
| parent_id | int(11) unsigned | NO   | MUL | NULL    |       |
| child_id  | int(11) unsigned | NO   |     | NULL    |       |
+-----------+------------------+------+-----+---------+-------+

Getting the count of related models

Hi,

Firstly, amazing package! Thanks so much.

I'm trying to use it to get the total counts of related models. Consider the following example... a hierarchical areas table that I use the tree method on e.g.

Area::tree()->depthFirst()->get()

So you end up with something like this:

California
    -> Los Angeles
        -> Hollywood
        -> Santa Monica

Now imagine a properties table that is related to area via area_id and I want the number of properties for each area. I can use ->withCount("properties"), however that will only give the count of properties for that area. It won't include descendants.

Let's say Los Angeles has 1 property, while Hollywood has 2 and Santa Monica has 3. Using a simple withCount would give this:

California (0)
    -> Los Angeles (1)
        -> Hollywood (2)
        -> Santa Monica (3)

When in reality, what I'm after, is this:

California (6)
    -> Los Angeles (6)
        -> Hollywood (2)
        -> Santa Monica (3)

Do you have any idea how I might go about fixing this problem using the package?

Thanks!

Multiple lists within one table

Is it possible to handle more than one list within one database table? There's an extra column list_id indicating to which list each element belongs.

Getting descendants of a given element seems to work but getting the whole tree silently fails with an empty result.

Data too long for column 'path' at row 1

If you're using MySQL in strict mode I think recursive query results an error if it has more than x characters.

I managed to fix it when I changed the query for casting 255 characters but it may end up with just getting ie 50-60 results which we don't prefer.

Any suggestions on how to fix it without disabling the strict mode?

possible to get a sub tree?

Hi Jonas,

Is it possible to get a sub tree from an existing node?

$node = Node::find(3456);
$node->tree();

I assume this would return the same structure that tree does, but only underneath $node

Also, is it possible to load children recursively? I originally thought that's what tree() would do, but I was wrong, tree() returns a flat list.

Unknown column 'laravel_cte.id'

Hello,
Here you can see my user table schema,

Schema::create('users', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->string('surname');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->unsignedBigInteger('parent_id')->index()->nullable();
            $table->string('password');           
            $table->timestamps();
            $table->softDeletes();
        });

Here is my code,

 $qb = QueryBuilder::for(auth()->user()->descendants()->getQuery())->with([
            'roles',
            'phone',
            'agent:id,name,surname',
            'referral_tags' => fn($query) => $query->where([
                'user_id' => auth()->id(),
                'is_set' => 1,
            ]),
            'referral_tags.tags'
        ])->withCount('descendants')
            ->allowedFilters([
                AllowedFilter::callback('path', function ($query) use ($filter) {
                    $query->where('path', 'like', '%.' . $filter['path']);
                    $query->orWhere('path', 'like', '%.' . $filter['path'] . '.%');
                    $query->orWhere('path', 'like', $filter['path'] . '.%');
                    $query->orWhere('path', 'like', $filter['path']);
                    $query->where('path', '!=', null);
                }),
                AllowedFilter::callback('referral_tag_id', fn($query) => $query->whereHas('referral_tags', function ($q) use ($filter) {
                    $q->whereIn('id', explode(',', $filter['referral_tag_id']));
                })),
                AllowedFilter::callback('role_id', fn($query) => $query->whereHas('roles', function ($q) use ($filter) {
                    $q->whereIn('role_id', explode(',', $filter['role_id']));
                })),
                AllowedFilter::callback('phone', fn($query) => $query->whereHas('phone', function ($q) use ($filter) {
                    $q->where('phone', 'like', '%' . $filter['phones'] . '%');
                })),
                AllowedFilter::callback('name', function ($query) use ($filter) {

                    if (!isset($filter['path'])) {
                        $query->where('name', 'like', '%' . $filter['name'] . '%');
                        $query->orWhere('surname', 'like', '%' . $filter['name'] . '%');
                    } else {
                        $query->where('path', 'like', '%.' . $filter['path']);
                        $query->orWhere('path', 'like', '%.' . $filter['path'] . '.%');
                        $query->where('path', '!=', null);
                        $query->where(function ($query) use ($filter) {
                            $query->where('name', 'like', '%' . $filter['name'] . '%')
                                ->orWhere('surname', 'like', '%' . $filter['name'] . '%');
                        });
                    }


                }),
                AllowedFilter::callback('role', fn($query) => $query->whereHas('roles', function ($q) use ($filter) {
                    $q->whereIn('role_id', explode(',', $filter['role']));
                })),
                'email'
            ]);

        return $qb;

Erro:
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'laravel_cte.id' in 'where clause' (SQL: with recursive laravel_cteas ((select *, 1 asdepth, cast(idas char(65535)) aspathfromuserswhereusers.parent_id= 2 andusers.parent_idis not null) union all (selectusers.*, depth+ 1 asdepth, concat(path, ., users.id) from usersinner joinlaravel_cteonlaravel_cte.id=users.parent_id)) select laravel_cte.*, (with recursive laravel_cteas ((select *, 1 asdepth, cast(idas char(65535)) aspathfromuserswhereusers.parent_id=laravel_cte.id) union all (select users.*, depth+ 1 asdepth, concat(path, ., users.id) from usersinner joinlaravel_cteonlaravel_cte.id=users.parent_id)) select count(*) from laravel_ctewherelaravel_cte.deleted_atis null) asdescendants_countfromlaravel_ctewhere exists (select * fromrolesinner joinmodel_has_rolesonroles.id=model_has_roles.role_idwherelaravel_cte.id=model_has_roles.model_idandmodel_has_roles.model_type= App\Models\User androles.idin (7)) andlaravel_cte.deleted_atis null limit 15 offset0)``

The test server does not have this error, the production has

Test server:
Laravel 8,
Php 7.4
mysql Ver 8.0.21 for Linux on x86_64 (MySQL Community Server - GPL)

Prod server
Laravel 8,
Php 7.4
mysql Ver 15.1 Distrib 10.3.22-MariaDB, for Linux (x86_64) using readline 5.1

Thanks

Find depth by passing ID

Hi there, hope you are fine!
We are trying to find the depth by passing the id as below,

$id= 9;
$x= MissionTerritory::where('id', $id)->tree()->depthFirst()->first();
dd($x->depth);

The above method we can get the depth(loading time seems a little high) but is there any other better way?

Error on: MySQL server version for the right syntax to use near 'recursive `laravel_cte`

SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'recursive laravel_cte as ((select , 1 as depth, cast(id as char(65535)) a' at line 1 (SQL: with recursive laravel_cte as ((select , 1 as depth, cast(id as char(65535)) as path from members where members.parent_id = 1 and members.parent_id is not null) union all (select members., depth + 1 as depth, concat(path, ., members.id) from members inner join laravel_cte on laravel_cte.id = members.parent_id)) select count() as aggregate from laravel_cte) (View: *****)

This error showed up when uploaded on the cpanel yet on my local machine it works perfectly.
What must be the solution. Please help. Thank you in advanced.

Followup question: my cpanel is using 5.6 mysql version, is there any possibilities that these errors happens because of the version I am using?

Polymorphic self referencing support

I assume this package doesn't support this, and I'll have to use the laravel cte package directly.

Let's say I have two classes: Parent and Child. The Child class either belongs to a Parent, or another Child, through a polymorphic relationship.

Parent
id - integer

Child
id - integer
groupable_id - integer
groupable_type - string (either Child or Parent).

The problem with this is, when I use HasRecursiveRelationships on the Child model, and return groupable_id from getParentKeyName(), using $child->descendants; will also return any other Child models that have groupable_id that is equal to $child->id, even though that groupable_id may be referencing a Parent model and not $child.

Any support for this currently?

Tree with orderBy

Hi πŸ‘‹

I'm trying to use orderBy with tree()

Code

Product::query()
            ->tree()
            ->whereNull('parent_id')
            ->orderBy(\DB::raw('FIELD(type, "type1", "type2", "type3")'))
            ->orderBy('name')
            ->get()

In above code if I do not specify condition for parent_id then it returns all records at first level

Also no effect of orderBy in the result of above query

[Error] Recursive queries when using "Eager Loading By Default"

Version: 1.3
Laravel version: 6.x
Expected: Only one CTE query is executed

When using a default relation it causes that more than one query is executed.

    /**
     * The relationships that should always be loaded.
     *
     * @var array
     */
    protected $with = ['author'];

How to get depth value with tree ()

$categories = Category::select('id', 'title')->tree()->depthFirst()->get();

How to get depth value?

Is there something like
$categories[2]->depth;

How to add where clause ?

suppose I want to add where clause at Model::where('id', $id)->descendants. How to achieve that ? Also how to select only specific columns not * ?

recursive seems not working

Hi Jonas.
It seems something is broken. All queries that tries to go recursive I have error:
Illuminate/Database/QueryException with message 'SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'recursive laravel_cte as ((select *, 0 as depth, cast(id as char(65535)) a' at line 1 (SQL: with recursive laravel_cte as ((select , 0 as depth, cast(id as char(65535)) as path from categories where parent_id is null) union all (select categories., depth + 1 as depth, concat(path, '.', categories.id) from categories inner join laravel_cte on laravel_cte.id = categories.parent_id)) select * from laravel_cte where laravel_cte.deleted_at is null)'.
Ancestors(), children(), parent(), siblings() are ok.
in my category model, I use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships; with softdelete
I'm using laravel 6.8. Category table fields are id, name and parent_id. with 40 records.

What's the best way to get relationship of all nodes in tree

I have a situation where Category has hierarchy and all categories have Posts.

class Category extends Model 
{
    use HasRecursiveRelationships;

    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }
}

class Post extends Model 
{
    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }
}

Is there a relationship I can put on Category to get the posts from itself and it's descendants?

Dynamically change parent key name in model.

Hi how can i dynamically change the parent key name in my model?

Right now I override the getParentKeyName method in my model but what if I need to dynamically change it because in my project I have two features in which they have different parent key name.

Thanks you.

Single query to get all descants on particular depth of multiple root ids

Closed Issue here

Hi, your package is awesome but we have another issue from the closed issue as mentioned the top,
We have tree with multiple roots, among which we find data based on particular depth (say eg 5) and paginate over it. Currently, we are using the below code.

$root=Category::isRoot()->pluck('id');
$categories = Category::findMany($root);
  foreach ($categories as $category) {
       $result[] = $category->descendants()->whereDepth(5)->with('ancestors')->get()->toArray();
  }

The above code works great but it's time-consuming. So we are in need to make them a single query to find depth with multiple roots. Surely this paginate will make everything smoother. Can you please let us know the possibilities with the query.

Thank you

Adding filter CTE

Is there a possibility to add a filter to the cte block,
I have multiple hierarchies of companies, in a table contractor_hierarchy_relation with columns;

  • contractor_hierarchy_id
  • contractor_id
  • parent_contractor_id

contractor_hierarchy_id is a foreign key that I'd like to use in a where condition within the cte, while fetching the descendants and ascendants;
The query generated while fetching the descendants is;
`WITH RECURSIVE laravel_cte AS (
(
SELECT ,
1 AS depth,
cast(contractor_id AS char(65535)) AS path
FROM contractor_hierarchy_relation
WHERE contractor_hierarchy_relation.parent_contractor_id in (?)
)
UNION ALL
(
SELECT contractor_hierarchy_relation.
,
depth + 1 AS depth,
concat(path, ?, contractor_hierarchy_relation.contractor_id)
FROM contractor_hierarchy_relation
INNER JOIN laravel_cte ON laravel_cte.contractor_id = contractor_hierarchy_relation.parent_contractor_id
)
)
SELECT *
FROM laravel_cte

I'd like to add a filter for the contractor_hierarchy_id column in the cte as highlighted in the query below;
`WITH RECURSIVE laravel_cte AS (
(
SELECT ,
1 AS depth,
cast(contractor_id AS char(65535)) AS path
FROM contractor_hierarchy_relation
WHERE contractor_hierarchy_relation.parent_contractor_id in (?) AND
contractor_hierarchy_relation.contractor_hierarchy_id = (?)
)
UNION ALL
(
SELECT contractor_hierarchy_relation.
,
depth + 1 AS depth,
concat(path, ?, contractor_hierarchy_relation.contractor_id)
FROM contractor_hierarchy_relation
INNER JOIN laravel_cte ON laravel_cte.contractor_id = contractor_hierarchy_relation.parent_contractor_id
AND contractor_hierarchy_relation.contractor_hierarchy_id = (?)
)
)
SELECT *
FROM laravel_cte

Is there any provision to achieve this behavior?

Would this package work with two parent_id? (father_id, mother_id)

Title stated the question, here's a table drawing my question:

id father_id mother_id
1
2
3 1 2

Wondering if it's possible to do something like User::find(3)->descendantsAndSelf() where you would get a tree of the node sibling with it's parents, greatparents etc..

Ref: Getting Descendant of Ancestor with Descendant in a single query #10

Closed Issue here

Hi, your package is awesome but we have another issue from the closed issue as mentioned the top,
We have tree with multiple roots, among which we find data based on particular depth (say eg 5) and paginate over it. Currently, we are using the below code.

$root=Category::isRoot()->pluck('id');
$categories = Category::findMany($root);
  foreach ($categories as $category) {
       $result[] = $category->descendants()->whereDepth(5)->with('ancestors')->get()->toArray();
  }

The above code works great but it's time-consuming. So we are in need to make them a single query to find depth with multiple roots. Surely this paginate will make everything smoother. Can you please let us know the possibilities with the query.

Thank you

Is there a way to generate a nested list?

Hi,
I love the library, just a quick question. Is there a way to generate a nested list for a given root ID please?
I've got a table like
Site table
id | name | is_site_to

and getParentKeyName() to return is_site to, so that all works

But when I call

Site::find(1)->descendants->get()

Then I get a flat list of sites and their descendants, is there any way to make this nested please?

error when model id with string(uuid)

class CreateUserGroupsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('user_groups', function (Blueprint $table) {
            if (env("DB_CONNECTION") == "mysql") {
                $table->engine = 'InnoDB';
            }
            $table->char('id', 32)->primary()->comment("UUID");
            $table->string('name')->unique();
            $table->string('desc')->nullable();
            $table->boolean('locked')->default(false);
            $table->softDeletes();
            $table->timestamps();
            $table->char('parent_id', 32)->nullable()->index();
            $table->foreign('parent_id')->references('id')->on('parent_id')->onDelete('cascade');
        });
    }
}

SQLSTATE[42804]: Datatype mismatch: 7 ERROR: recursive query "laravel_cte" column 13 has type character(32)[] in non-recursive term but type bpchar[] overall LINE 1: ...rsive "laravel_cte" as ((select *, -1 as "depth", array["id"... ^ HINT: Cast the output of the non-recursive term to the correct type. (SQL: with recursive "laravel_cte" as ((select , -1 as "depth", array["id"] as "path" from "user_groups" where "user_groups"."id" = 8efec07320e744c9a17e7de5fca1bb6c) union all (select "user_groups"., "depth" - 1 as "depth", "path" || "user_groups"."id" from "user_groups" inner join "laravel_cte" on "laravel_cte"."parent_id" = "user_groups"."id")) select * from "laravel_cte" where "laravel_cte"."deleted_at" is null)

hasManyOfDescendantsAndSelf not working

class Tree extends Model {

use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;

public function getLocalKeyName()
    {
        return 'link';
    }

    public function getParentKeyName()
    {
        return 'parent_link';
    }

public function recursiveGoods(){
        return $this->hasManyOfDescendantsAndSelf('\App\FixModels\Good', 'parent_link', 'link');
    }

}
use Staudenmeir\LaravelCte\Eloquent\QueriesExpressions;

class Good extends Model // parent_link
{

    use QueriesExpressions;

}
public static function sidebar(){
        $data = self::where('parent_link', '=', "00000000-0000-0000-0000-000000000000")
            ->withCount('recursiveGoods')
//            ->with('img')
            ->get();
        dd($data);
    }

And error

SQLSTATE[42S22]: Column not found: 1054 Unknown column 'trees.link' in 'where clause' (SQL: select trees.*, (with recursive laravel_cteas ((select * fromtreesaslaravel_reserved_0wherelaravel_reserved_0.link=trees.link) union all (select trees.* from treesinner joinlaravel_cteonlaravel_cte.link=trees.parent_link)) select count(*) from goodswheregoods.parent_linkin (selectlinkfromlaravel_cte)) as recursive_goods_countfromtreeswhereparent_link = 00000000-0000-0000-0000-000000000000)

Strange results when querying relationships

Hi @staudenmeir

Thanks so much for the great package!

I have run into a problem that I'm hoping is just something that I'm missing... Essentially, I have a Page model which utilises your package, and has a status field to indicate whether it is enabled or not.

I am now trying to to get a list of all pages which are enabled (where status = 1) where all their ancestors are also enabled. My gut feeling was to use a query such as this:

Page::whereDoesntHave('ancestorsAndSelf', function ($query) {
    $query->where('status', 0);
})->get();

This returns a list of all enabled pages, but does not filter pages who have ancestors who are disabled. For example, given the following pages:

[
     [
       "id" => 1,
       "parent_id" => null,
       "status" => 1,
     ],
     [
       "id" => 2,
       "parent_id" => 1,
       "status" => 0,
     ],
     [
       "id" => 3,
       "parent_id" => 2,
       "status" => 1,
     ],
]

I would expect my query above to return only the page with id of 1, but instead it returns both pages 1 and 3.

Am I missing something, or is this a limitation? If it's the latter, do you have a suggestion for how I could produce such a list?

Thanks so much!

Collision with eloquent-eager-limit

Getting this when I add both the HasRecursiveRelationships and HasEagerLimit to a model:

Trait method newBaseQueryBuilder has not been applied, because there are collisions with other trait methods on App\Category

Any way to make the two play nice? I don't need them to work on the same relationship, but different relationships on the same model.

belongsToMany Support?

Is there a way of suporting belongs to may relationships?

class Group
{
    use HasRecursiveRelationships;

    public function students()
    {
        return $this->belongsToMany(Student::class);
    }
}

The group structure looks something like this:

- Darwin High School
    -- Monday
        --- Guitar
    -- Tuesday
        --- Ukele

A student would be able to be placed into both Guitar and Ukele classes.

I'd like to be able to get all students who are at Darwin High School.

Additional where condition for ancestors

Hi, @staudenmeir
I want to thank you again for the wonderful library.

But I have next difficulty:
I store different binary structures in the one table.

In this table I store every person as

  • ID - his unique identifier for this table
  • GROUP_ID - identifier of group (like identifier of binary structure)
  • USER_ID - his real identifier from main business system
  • PARENT_ID - unique identifier of his parent according to binary structure
  • REFERRER_ID - real identifier of his referrer from main business system

So, table looks like

ID PARENT_ID GROUP_ID USER_ID REFERRER_ID
1 null 1 200 null
2 null 2 300 null
3 2 2 301 300
10 9 2 310 300

And now main question:
I want to get all referrers of my last "node".
Of course, i have changed parentKey and localKey to USER_ID and REFERRER_ID accordingly.
But, as you can see in my case, I have to append to where conditions additional string.

I will try to explain as bold text:

with recursive "laravel_cte" as (
select *
from (
select , 0 as "depth", cast("user_id" as text) as "path"
from "table"
where "table"."user_id" = 309
AND "table"."group_id" = 2
)
union all
select "table".
,
"depth" - 1 as "depth",
"path" || '.' || "table"."user_id"
from "table"
inner join "laravel_cte" on "laravel_cte"."referrer_id" = "table"."user_id"
AND "laravel_cte"."group_id" = "table"."group_id"
)

Naturally, I clearly understand that my case is very rare, but maybe there is some solution?

Trouble overriding the parent key name on runtime

Hey there,

I've been trying with no success to understand what I'm doing wrong while overriding the 'parent key name' dynamically. For the sake of example lets say I have a 'Post' model with these fields parent_id and original_post_id.
When I need to get the ancestors or children related with parent_id everything works fine, but when using the original_post_id everything breaks up.

// Post Model
class Post extends Model
{
    use HasRecursiveRelationships;

    protected $parentKey;

    public function getParentKeyName()
    {
        return $this->parentKey;
    }

    public function setParentKeyName($parentKey)
    {
        $this->parentKey = $parentKey;

        return $this;
    }
}

// usage
$posts = (new Post)->setParentKeyName('original_post_id')->with(['ancestors', 'some_other_suff'])->get();

I get the following error:

SQLSTATE[42601]: Syntax error: 7 ERROR: zero-length delimited identifier at or near """" LINE 1: ...posts" inner join "laravel_cte" on "laravel_cte"."" = "pos... ^ (SQL: with recursive "laravel_cte" as ((select *, -1 as "depth", array[("id" || '')::varchar] as "path" from "posts" where "posts"."id" in (680e8948-3932-4a31-86ba-a80914cbb9ae, b405eaa0-dd78-4bf7-bc50-3c0135e97307, f74c2829-f4d6-45ef-ab91-ab10804e1ce6)) union all (select "posts".*, "depth" - 1 as "depth"..., 

The strange thing is if the getParentKeyName method is hard coded to return 'original_post_id' it works fine.

Is it what I'm trying to do kinda stupid as in doesn't make sense or impossible πŸ˜… ?

Thank you in advance.

Query conditions like orderBy

Hi, Awesome package
My Code
$demo = Demo::tree(2)->whereDepth(2)->with('ancestors')->get();

I need to do something like the following
$demo = Demo::tree(2)->whereDepth(2)->with('ancestors')->get();

Note: It has to be ordered for every level on the tree, so is there any chance?

rootAncestor return null

Hey im tring to get rootAncestor from child category but its return null everytime
28 is a id of iPhone category which is Leaf node and its parent are Apple -> Laptop -> Electronics & Home Appliances
$mostParentAncestor = Category::find(28)->rootAncestor;
dd($mostParentAncestor); // returning null

result should be Electronics & Home Appliances

laravel version 6
PHP 7.3.8

Unknown SQL error

I got encountered error as follow:

SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'recursive `laravel_cte` as ((select *, -1 as `depth`, cast(`id` as char(65535)) ' at line 1 (SQL: with recursive `laravel_cte` as ((select *, -1 as `depth`, cast(`id` as char(65535)) as `path` from `users` where `users`.`id` = 1) union all (select `users`.*, `depth` - 1 as `depth`, concat(`path`, '.', `users`.`id`) from `users` inner join `laravel_cte` on `laravel_cte`.`sponsor_user_id` = `users`.`id`)) select * from `laravel_cte`)

[Question] How to get edge nodes?

Hi, @staudenmeir. Thank you for this awesome library!

I have classic binary tree in the database:
1
2 3
4 5 6 7

How can I retrieve only edge descendants nodes of the root node:
β€” for the left side: [2, 4]
β€” for the right side: [3, 7]

0r, as example, how to get extremely bottom node at left or right corner of the tree, respectively, 4 or 7 (in any depth of the tree)?

Can I append additional where condition to the exactly recursive CTE (... and branch = β€œleft”)?

Thanks!

Use different primary for tree

How can I change primary key for selecting?

I have table like
tree_id | link | parent_link

so primary key tree_id is not a leaf identifier.
I can change name for parent_link, and want to change tree_id to link

tree dont work: No query for results

HI !
Im using your package and the Trait tree dont work !

In my case $tree = CompanySector::tree()->get();
error: "No query results for Model CompanySector.

The other features are ok

filter user descendants and show level/depth

Laravel version: ^7.24
Package version: ~1.4
PHP version: 7.3

I need to get users where depth/level is < 10 , and I want to filter or add a where condition on the descendants
which is active = 1 and show their depth/level and orderBy depth/level

here is my query:

$id = 1; // me
$users = User::find($id)->descendants()->whereDepth('<', 10)->get();

where I'm user id 1

ID   |    Name   |     Parent ID    |   Depth/Level  
2        john            1                 1         
3        jane            2                 2             

Edit: I found out that adding ->depth in the loop will show the depth/level now I only want to know how to add the where condition on the descendants

Syntax error or access violation: 1064

When I try to fetch data such as:
dd(ConfluencePage::with('descendants')->first()); I get this error

SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'recursive `laravel_cte` as ((select *, 1 as `depth`, cast(`id` as char(65535)) a' at line 1 (SQL: with recursive `laravel_cte` as ((select *, 1 as `depth`, cast(`id` as char(65535)) as `path` from `confluence_pages` where `confluence_pages`.`parent_confluence_page_id` in (1)) union all (select `confluence_pages`.*, `depth` + 1 as `depth`, concat(`path`, '.', `confluence_pages`.`id`) from `confluence_pages` inner join `laravel_cte` on `laravel_cte`.`id` = `confluence_pages`.`parent_confluence_page_id`)) select * from `laravel_cte`)

I checked for special column names that could violate syntax but no success there. In model I just used the package trait and overridden getParentKeyName() method:

public function getParentKeyName()
{
    return 'parent_confluence_page_id';
}

Paginate links of hasManyOfDescendantsAndSelf

hasManyOfDescendantsAndSelf return collection
How to get pagination links?

    public function recursiveProducts()
    {
        return $this->hasManyOfDescendantsAndSelf(Product::class, 'node_id')
            ->where('sp_unit_price', '>', 0)
            ->where('active', '=', true);
    }
            ->with(["recursiveProducts"=>function ($query) {
                $limit = request('per_page', 12);
                $query->paginate($limit);
            }])

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.