Skip to content

Commit

Permalink
Merge branch '1.12'
Browse files Browse the repository at this point in the history
# Conflicts:
#	src/IdeHelper/RecursiveRelationsHook.php
#	tests/IdeHelper/RecursiveRelationsHookTest.php
  • Loading branch information
staudenmeir committed Dec 30, 2023
2 parents e6bb40e + 48d0b84 commit c8f69f6
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 7 deletions.
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ class User extends Model

#### Nested Results

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

```php
$users = User::tree()->get();
Expand Down Expand Up @@ -696,6 +696,7 @@ Supports Laravel 9+.
- [Depth](#graphs-depth)
- [Path](#graphs-path)
- [Custom Paths](#graphs-custom-paths)
- [Nested Results](#graphs-nested-results)
- [Recursive Query Constraints](#graphs-recursive-query-constraints)
- [Known Issues](#graphs-known-issues)

Expand Down Expand Up @@ -1013,6 +1014,46 @@ class Node extends Model
}
```

#### <a name="graphs-nested-results">Nested Results</a>

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

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

$tree = $nodes->toTree();
```

This recursively sets `children` relationships:

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

#### <a name="graphs-recursive-query-constraints">Recursive Query Constraints</a>

You can add custom constraints to the CTE's recursive query. Consider a query where you want to traverse a node's
Expand Down
50 changes: 50 additions & 0 deletions src/Eloquent/Graph/Collection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace Staudenmeir\LaravelAdjacencyList\Eloquent\Graph;

use Illuminate\Database\Eloquent\Collection as Base;

/**
* @template TKey of array-key
* @template TModel of \Illuminate\Database\Eloquent\Model
*
* @extends \Illuminate\Database\Eloquent\Collection<TKey, TModel>
*/
class Collection extends Base
{
/**
* Generate a nested tree.
*
* @param string $childrenRelation
* @return static<int, TModel>
*/
public function toTree(string $childrenRelation = 'children'): static
{
if ($this->isEmpty()) {
return $this;
}

$parentKeyName = $this->first->relationLoaded('pivot')
? 'pivot.' . $this->first()->getParentKeyName()
: 'pivot_' . $this->first()->getParentKeyName();
$localKeyName = $this->first()->getLocalKeyName();
$depthName = $this->first()->getDepthName();

$depths = $this->pluck($depthName);

$graph = new static(
$this->where($depthName, $depths->min())->values()
);

$itemsByParentKey = $this->groupBy($parentKeyName);

foreach ($this->items as $item) {
$item->setRelation(
$childrenRelation,
$itemsByParentKey[$item->$localKeyName] ?? new static()
);
}

return $graph;
}
}
12 changes: 12 additions & 0 deletions src/Eloquent/Traits/HasGraphAdjacencyList.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Staudenmeir\LaravelAdjacencyList\Eloquent\Graph\Collection;
use Staudenmeir\LaravelAdjacencyList\Eloquent\Relations\Graph\Ancestors;
use Staudenmeir\LaravelAdjacencyList\Eloquent\Relations\Graph\Descendants;

Expand Down Expand Up @@ -421,6 +422,17 @@ public function newEloquentBuilder($query)
return new \Staudenmeir\LaravelAdjacencyList\Eloquent\Builder($query);
}

/**
* Create a new Eloquent Collection instance.
*
* @param array $models
* @return \Staudenmeir\LaravelAdjacencyList\Eloquent\Graph\Collection
*/
public function newCollection(array $models = [])
{
return new Collection($models);
}

/**
* Set an additional constraint for the recursive query.
*
Expand Down
8 changes: 4 additions & 4 deletions src/IdeHelper/RecursiveRelationsHook.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

use Barryvdh\LaravelIdeHelper\Console\ModelsCommand;
use Barryvdh\LaravelIdeHelper\Contracts\ModelHookInterface;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Staudenmeir\LaravelAdjacencyList\Eloquent\Collection;
use Staudenmeir\LaravelAdjacencyList\Eloquent\Collection as TreeCollection;
use Staudenmeir\LaravelAdjacencyList\Eloquent\Graph\Collection as GraphCollection;
use Staudenmeir\LaravelAdjacencyList\Eloquent\HasGraphRelationships;
use Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;

Expand Down Expand Up @@ -135,7 +135,7 @@ public function run(ModelsCommand $command, Model $model): void
if (in_array(HasRecursiveRelationships::class, $traits)) {
foreach (static::$treeRelationships as $relationship) {
$type = $relationship['manyRelation']
? '\\' . Collection::class . '|\\' . $model::class . '[]'
? '\\' . TreeCollection::class . '|\\' . $model::class . '[]'
: '\\' . $model::class;

$this->addRelationship($command, $relationship, $type);
Expand All @@ -144,7 +144,7 @@ public function run(ModelsCommand $command, Model $model): void

if (in_array(HasGraphRelationships::class, $traits)) {
foreach (static::$graphRelationships as $relationship) {
$type = '\\' . EloquentCollection::class . '|\\' . $model::class . '[]';
$type = '\\' . GraphCollection::class . '|\\' . $model::class . '[]';

$this->addRelationship($command, $relationship, $type);
}
Expand Down
96 changes: 96 additions & 0 deletions tests/Graph/CollectionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

namespace Staudenmeir\LaravelAdjacencyList\Tests\Graph;

use Illuminate\Database\Eloquent\Builder;
use Staudenmeir\LaravelAdjacencyList\Tests\Graph\Models\Node;
use Staudenmeir\LaravelAdjacencyList\Tests\Graph\Models\NodeWithCycleDetection;
use Staudenmeir\LaravelAdjacencyList\Tests\Graph\Models\NodeWithCycleDetectionAndStart;

class CollectionTest extends TestCase
{
public function testToTree()
{
if ($this->database === 'sqlsrv') {
$this->markTestSkipped();
}

$constraint = fn (Builder $query) => $query->whereIn('id', [2, 3]);

$nodes = Node::subgraph($constraint)->orderBy('id')->get();

$graph = $nodes->toTree();

$this->assertEquals([2, 3], $graph->pluck('id')->all());
$this->assertEquals([5], $graph[0]->children->pluck('id')->all());
$this->assertEquals([7, 8], $graph[0]->children[0]->children->pluck('id')->all());
$this->assertEquals([8], $graph[0]->children[0]->children[0]->children->pluck('id')->all());
$this->assertEquals([6], $graph[1]->children->pluck('id')->all());
}

public function testToTreeWithRelationship()
{
$nodes = Node::find(2)->descendants()->orderBy('id')->get();

$graph = $nodes->toTree();

$this->assertEquals([5], $graph->pluck('id')->all());
$this->assertEquals([7, 8], $graph[0]->children->pluck('id')->all());
$this->assertEquals([8], $graph[0]->children[0]->children->pluck('id')->all());
}

public function testToTreeWithCycle()
{
if ($this->database === 'sqlsrv') {
$this->markTestSkipped();
}

$this->seedCycle();

$constraint = fn (Builder $query) => $query->where('id', 12);

$nodes = NodeWithCycleDetection::subgraph($constraint)->orderBy('id')->get();

$graph = $nodes->toTree();

$this->assertEquals([12], $graph->pluck('id')->all());
$this->assertEquals([13], $graph[0]->children->pluck('id')->all());
$this->assertEquals([14], $graph[0]->children[0]->children->pluck('id')->all());
$this->assertEmpty($graph[0]->children[0]->children[0]->children);
}

public function testToTreeWithCycleAndStart()
{
if ($this->database === 'sqlsrv') {
$this->markTestSkipped();
}

$this->seedCycle();

$constraint = fn (Builder $query) => $query->where('id', 12);

$nodes = NodeWithCycleDetectionAndStart::subgraph($constraint)->orderBy('id')->get();

$graph = $nodes->toTree();

$this->assertEquals([12], $graph->pluck('id')->all());
$this->assertEquals([13], $graph[0]->children->pluck('id')->all());
$this->assertEquals([14], $graph[0]->children[0]->children->pluck('id')->all());
$this->assertEquals([12], $graph[0]->children[0]->children[0]->children->pluck('id')->all());
}

public function testToTreeWithEmptyCollection()
{
if ($this->database === 'sqlsrv') {
$this->markTestSkipped();
}

$constraint = fn (Builder $query) => $query->where('id', 1);

$nodes = Node::subgraph($constraint)->where('id', 0)->orderBy('id')->get();

$graph = $nodes->toTree();

$this->assertEmpty($graph);
}
}
2 changes: 1 addition & 1 deletion tests/IdeHelper/RecursiveRelationsHookTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public function testGraphRelations()
$command->shouldReceive('setProperty')->times(2);
$command->shouldReceive('setProperty')->once()->with(
'ancestorsAndSelf',
'\Illuminate\Database\Eloquent\Collection|\Staudenmeir\LaravelAdjacencyList\Tests\IdeHelper\Models\Node[]',
'\Staudenmeir\LaravelAdjacencyList\Eloquent\Graph\Collection|\Staudenmeir\LaravelAdjacencyList\Tests\IdeHelper\Models\Node[]',
true,
false,
"The node's recursive parents and itself.",
Expand Down
2 changes: 1 addition & 1 deletion tests/Tree/CollectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public function testToTree()
$this->assertEquals([12], $tree[1]->children->pluck('id')->all());
}

public function testToTreeWithDescendants()
public function testToTreeWithRelationship()
{
$users = User::find(1)->descendants()->orderBy('id')->get();

Expand Down

0 comments on commit c8f69f6

Please sign in to comment.