Skip to content

Commit 68a3320

Browse files
committed
lazychaser#45: descendants is now a relation that can be eagerly loaded
1 parent 45e3dfc commit 68a3320

7 files changed

+237
-23
lines changed

CHANGELOG.markdown

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* The number of missing parent is now returned when using `countErrors`
1313
* #79: implemented scoping feature
1414
* #81: moving node now makes model dirty before saving it
15+
* #45: descendants is now a relation that can be eagerly loaded
1516

1617
### 3.1.1
1718

README.markdown

+4-6
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,11 @@ and the node that we are manipulating. It can be a fresh model or one from datab
5252

5353
### Relationships
5454

55-
Node has two predefined relationships: `parent` and `children`. They are fully
56-
functional, except for Laravel's `has` due to limitations of the framework.
57-
You can use `hasChildren` or `hasParent` to apply constraints:
55+
Node has following relationships that are fully functional and can be eagerly loaded:
5856

59-
```php
60-
$items = Category::hasChildren()->get();
61-
```
57+
- Node belongs to `parent`
58+
- Node has many `children`
59+
- Node has many `descendants`
6260

6361
### Inserting nodes
6462

src/DescendantsRelation.php

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
<?php
2+
3+
namespace Kalnoy\Nestedset;
4+
5+
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
6+
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
7+
use Illuminate\Database\Eloquent\Model;
8+
use Illuminate\Database\Eloquent\Relations\Relation;
9+
use Illuminate\Database\Query\Builder;
10+
use InvalidArgumentException;
11+
12+
class DescendantsRelation extends Relation
13+
{
14+
/**
15+
* @var QueryBuilder
16+
*/
17+
protected $query;
18+
19+
/**
20+
* @var NodeTrait|Model
21+
*/
22+
protected $parent;
23+
24+
/**
25+
* DescendantsRelation constructor.
26+
*
27+
* @param QueryBuilder $builder
28+
* @param Model $model
29+
*/
30+
public function __construct(QueryBuilder $builder, Model $model)
31+
{
32+
if ( ! NestedSet::isNode($model)) {
33+
throw new InvalidArgumentException('Model must be node.');
34+
}
35+
36+
parent::__construct($builder, $model);
37+
}
38+
39+
/**
40+
* @param EloquentBuilder $query
41+
* @param EloquentBuilder $parent
42+
* @param array $columns
43+
*
44+
* @return mixed
45+
*/
46+
public function getRelationQuery(EloquentBuilder $query, EloquentBuilder $parent,
47+
$columns = [ '*' ]
48+
) {
49+
$query->select($columns);
50+
51+
$table = $query->getModel()->getTable();
52+
53+
$query->from($table.' as '.$hash = $this->getRelationCountHash());
54+
55+
$table = $this->wrap($table);
56+
$hash = $this->wrap($hash);
57+
$lft = $this->wrap($this->parent->getLftName());
58+
$rgt = $this->wrap($this->parent->getRgtName());
59+
60+
return $query->whereRaw("{$hash}.{$lft} between {$table}.{$lft} + 1 and {$table}.{$rgt}");
61+
}
62+
63+
/**
64+
* Get a relationship join table hash.
65+
*
66+
* @return string
67+
*/
68+
public function getRelationCountHash()
69+
{
70+
return 'self_'.md5(microtime(true));
71+
}
72+
73+
/**
74+
* Set the base constraints on the relation query.
75+
*
76+
* @return void
77+
*/
78+
public function addConstraints()
79+
{
80+
if ( ! static::$constraints) return;
81+
82+
$this->query->whereDescendantOf($this->parent);
83+
}
84+
85+
/**
86+
* Set the constraints for an eager load of the relation.
87+
*
88+
* @param array $models
89+
*
90+
* @return void
91+
*/
92+
public function addEagerConstraints(array $models)
93+
{
94+
$this->query->whereNested(function (Builder $inner) use ($models) {
95+
// We will use this query in order to apply constraints to the
96+
// base query builder
97+
$outer = $this->parent->newQuery();
98+
99+
foreach ($models as $model) {
100+
$outer->setQuery($inner)->orWhereDescendantOf($model);
101+
}
102+
});
103+
}
104+
105+
/**
106+
* Initialize the relation on a set of models.
107+
*
108+
* @param array $models
109+
* @param string $relation
110+
*
111+
* @return array
112+
*/
113+
public function initRelation(array $models, $relation)
114+
{
115+
return $models;
116+
}
117+
118+
/**
119+
* Match the eagerly loaded results to their parents.
120+
*
121+
* @param array $models
122+
* @param EloquentCollection $results
123+
* @param string $relation
124+
*
125+
* @return array
126+
*/
127+
public function match(array $models, EloquentCollection $results, $relation)
128+
{
129+
foreach ($models as $model) {
130+
$descendants = $this->getDescendantsForModel($model, $results);
131+
132+
$model->setRelation($relation, $descendants);
133+
}
134+
135+
return $models;
136+
}
137+
138+
/**
139+
* Get the results of the relationship.
140+
*
141+
* @return mixed
142+
*/
143+
public function getResults()
144+
{
145+
return $this->query->get();
146+
}
147+
148+
/**
149+
* @param Model $model
150+
* @param EloquentCollection $results
151+
*
152+
* @return Collection
153+
*/
154+
protected function getDescendantsForModel(Model $model, EloquentCollection $results)
155+
{
156+
$result = $this->related->newCollection();
157+
158+
foreach ($results as $descendant) {
159+
if ($descendant->isDescendantOf($model)) {
160+
$result->push($descendant);
161+
}
162+
}
163+
164+
return $result;
165+
}
166+
}

src/NodeTrait.php

+3-5
Original file line numberDiff line numberDiff line change
@@ -259,11 +259,11 @@ public function children()
259259
/**
260260
* Get query for descendants of the node.
261261
*
262-
* @return QueryBuilder
262+
* @return DescendantsRelation
263263
*/
264264
public function descendants()
265265
{
266-
return $this->newScopedQuery()->whereDescendantOf($this->getKey());
266+
return new DescendantsRelation($this->newScopedQuery(), $this);
267267
}
268268

269269
/**
@@ -926,9 +926,7 @@ public function getAncestors(array $columns = array( '*' ))
926926
*/
927927
public function getDescendants(array $columns = array( '*' ))
928928
{
929-
return $this->newScopedQuery()
930-
->defaultOrder()
931-
->descendantsOf($this->getKey(), $columns);
929+
return $this->descendants()->get($columns);
932930
}
933931

934932
/**

tests/NodeTest.php

+41
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,47 @@ public function testRootNodesMoving()
705705

706706
$this->assertEquals(3, $node->getLft());
707707
}
708+
709+
public function testDescendantsRelation()
710+
{
711+
$node = $this->findCategory('notebooks');
712+
$result = $node->descendants;
713+
714+
$this->assertEquals(2, $result->count());
715+
$this->assertEquals('apple', $result->first()->name);
716+
}
717+
718+
public function testDescendantsEagerlyLoaded()
719+
{
720+
$nodes = Category::whereIn('id', [ 2, 5 ])->get();
721+
722+
$nodes->load('descendants');
723+
724+
$this->assertEquals(2, $nodes->count());
725+
$this->assertTrue($nodes->first()->relationLoaded('descendants'));
726+
}
727+
728+
public function testDescendantsRelationQuery()
729+
{
730+
$nodes = Category::has('descendants')->whereIn('id', [ 2, 3 ])->get();
731+
732+
$this->assertEquals(1, $nodes->count());
733+
$this->assertEquals(2, $nodes->first()->getKey());
734+
735+
$nodes = Category::has('descendants', '>', 2)->get();
736+
737+
$this->assertEquals(2, $nodes->count());
738+
$this->assertEquals(1, $nodes[0]->getKey());
739+
$this->assertEquals(5, $nodes[1]->getKey());
740+
}
741+
742+
public function testParentRelationQuery()
743+
{
744+
$nodes = Category::has('parent')->whereIn('id', [ 1, 2 ]);
745+
746+
$this->assertEquals(1, $nodes->count());
747+
$this->assertEquals(2, $nodes->first()->getKey());
748+
}
708749
}
709750

710751
function all($items)

tests/ScopedNodeTest.php

+16-6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public static function setUpBeforeClass()
1616
$schema->create('menu_items', function (\Illuminate\Database\Schema\Blueprint $table) {
1717
$table->increments('id');
1818
$table->unsignedInteger('menu_id');
19+
$table->string('title')->nullable();
1920
NestedSet::columns($table);
2021
});
2122

@@ -130,18 +131,14 @@ public function testSaveAsRoot()
130131

131132
$this->assertEquals(5, $node->getLft());
132133

133-
$node = MenuItem::find(3);
134-
135-
$this->assertEquals(1, $node->getLft());
134+
$this->assertOtherScopeNotAffected();
136135
}
137136

138137
public function testInsertion()
139138
{
140139
$node = MenuItem::create([ 'menu_id' => 1, 'parent_id' => 5 ]);
141140

142-
$node = MenuItem::find(3);
143-
144-
$this->assertEquals(1, $node->getLft());
141+
$this->assertOtherScopeNotAffected();
145142
}
146143

147144
/*
@@ -160,6 +157,19 @@ public function testDeletion()
160157

161158
$this->assertEquals(2, $node->getRgt());
162159

160+
$this->assertOtherScopeNotAffected();
161+
}
162+
163+
public function testMoving()
164+
{
165+
$node = MenuItem::find(1);
166+
$this->assertTrue($node->down());
167+
168+
$this->assertOtherScopeNotAffected();
169+
}
170+
171+
protected function assertOtherScopeNotAffected()
172+
{
163173
$node = MenuItem::find(3);
164174

165175
$this->assertEquals(1, $node->getLft());

tests/data/menu_items.php

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<?php return [
2-
[ 'id' => 1, 'menu_id' => 1, '_lft' => 1, '_rgt' => 2, 'parent_id' => null ],
3-
[ 'id' => 2, 'menu_id' => 1, '_lft' => 3, '_rgt' => 6, 'parent_id' => null ],
4-
[ 'id' => 5, 'menu_id' => 1, '_lft' => 4, '_rgt' => 5, 'parent_id' => 2 ],
5-
[ 'id' => 3, 'menu_id' => 2, '_lft' => 1, '_rgt' => 2, 'parent_id' => null ],
6-
[ 'id' => 4, 'menu_id' => 2, '_lft' => 3, '_rgt' => 6, 'parent_id' => null ],
7-
[ 'id' => 6, 'menu_id' => 2, '_lft' => 4, '_rgt' => 5, 'parent_id' => 4 ],
2+
[ 'id' => 1, 'menu_id' => 1, '_lft' => 1, '_rgt' => 2, 'parent_id' => null, 'title' => 'menu item 1' ],
3+
[ 'id' => 2, 'menu_id' => 1, '_lft' => 3, '_rgt' => 6, 'parent_id' => null, 'title' => 'menu item 2' ],
4+
[ 'id' => 5, 'menu_id' => 1, '_lft' => 4, '_rgt' => 5, 'parent_id' => 2, 'title' => 'menu item 3' ],
5+
[ 'id' => 3, 'menu_id' => 2, '_lft' => 1, '_rgt' => 2, 'parent_id' => null, 'title' => 'menu item 1' ],
6+
[ 'id' => 4, 'menu_id' => 2, '_lft' => 3, '_rgt' => 6, 'parent_id' => null, 'title' => 'menu item 2' ],
7+
[ 'id' => 6, 'menu_id' => 2, '_lft' => 4, '_rgt' => 5, 'parent_id' => 4, 'title' => 'menu item 3' ],
88
];

0 commit comments

Comments
 (0)