Skip to content

Commit 936157a

Browse files
authored
Merge pull request #41 from efureev/parentsByModel
feat: add `parentsByModelId` and `columnWithTbl` methods to Query Bui…
2 parents b742b5d + 107a921 commit 936157a

File tree

4 files changed

+222
-5
lines changed

4 files changed

+222
-5
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## [5.4.0](https://github.com/efureev/laravel-trees/compare/v5.3.0...v5.4.0) (2025-08-26)
4+
5+
### Added
6+
7+
- Method `parentsByModelId` for Query Builder. It allows you to get all parents of a model by its id (Without a main
8+
model). If you know `id` - you can select a list of parents. (Only 1 Query instead of 2)
9+
- Method `columnWithTbl` for Query Builder. It allows you to get a column with a table name
10+
311
## [5.3.0](https://github.com/efureev/laravel-trees/compare/v5.2.1...v5.3.0) (2025-03-08)
412

513
### Added

docs/ReceivingNodes.md

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ $parent = $node->parent;
3434
$parent = $node->parent()->first();
3535
```
3636

37-
### To get a Parents chains
37+
### To get Parents chains
3838

3939
> @return Collection
4040
@@ -126,3 +126,58 @@ $nextNode = $node->nextSibling()->first();
126126
$prevNode = $node->prev()->first();
127127
$nextNode = $node->next()->first();
128128
```
129+
130+
## Receiving through Queries without Models
131+
132+
### root
133+
134+
Returns a query for root nodes.
135+
136+
```php
137+
MultiCategory::root();
138+
```
139+
140+
### notRoot
141+
142+
Returns a query for non-root nodes.
143+
144+
```php
145+
MultiCategory::notRoot();
146+
```
147+
148+
### parentsByModelId
149+
150+
Returns a collection of parents of the node with the specified id.
151+
152+
NB: In progress. Works only for multi-tree nodes.
153+
154+
```php
155+
MultiCategory::parentsByModelId($node31->id)->get();
156+
MultiCategory::parentsByModelId($node31->id, level: 1)->get();
157+
MultiCategory::parentsByModelId($node31->id, andSelf: true)->get();
158+
```
159+
160+
### byTree
161+
162+
Returns a query for nodes of the specified tree.
163+
164+
```php
165+
MultiCategory::byTree($id);
166+
MultiCategory::byTree($id)->get();
167+
```
168+
169+
### toLevel
170+
171+
Returns a query for nodes of the specified level.
172+
173+
```php
174+
Category::toLevel(1);
175+
```
176+
177+
### byParent
178+
179+
Returns a query for nodes of the specified parent.
180+
181+
```php
182+
Category::byParent($pid);
183+
```

src/QueryBuilderV2.php

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Fureev\Trees;
66

7+
use Exception;
78
use Illuminate\Database\Eloquent\Builder;
89
use Illuminate\Database\Eloquent\Model;
910
use Illuminate\Database\Eloquent\ModelNotFoundException;
@@ -85,6 +86,48 @@ public function parents(?int $level = null, bool $andSelf = false): static
8586
->defaultOrder();
8687
}
8788

89+
public function parentsByModelId($modelId, ?int $level = null, bool $andSelf = false): static
90+
{
91+
if (!$this->model->isMulti()) {
92+
throw new Exception('Does not support single tree yet');
93+
}
94+
95+
$query = $this
96+
->joinSub(
97+
$this->model->newNestedSetQuery()->where('id', $modelId)->limit(1),
98+
't',
99+
function ($join) {
100+
$treeAttrName = (string)$this->model->treeAttribute();
101+
$join->on("t.$treeAttrName", '=', $this->columnWithTbl($treeAttrName));
102+
}
103+
);
104+
$condition = [
105+
106+
[
107+
$this->columnWithTbl((string)$this->model->leftAttribute()),
108+
$andSelf ? '<=' : '<',
109+
't.' . $this->model->leftAttribute(),
110+
],
111+
[
112+
$this->columnWithTbl((string)$this->model->rightAttribute()),
113+
$andSelf ? '>=' : '>',
114+
't.' . $this->model->rightAttribute(),
115+
],
116+
];
117+
118+
if ($level !== null) {
119+
$query->where(
120+
$this->columnWithTbl((string)$this->model->levelAttribute()),
121+
'>=',
122+
$level,
123+
);
124+
}
125+
126+
return $query
127+
->whereColumn($condition)
128+
->defaultOrder();
129+
}
130+
88131
/**
89132
* Get all descendants
90133
*
@@ -128,7 +171,7 @@ public function descendantsQuery(?int $level = null, bool $andSelf = false, bool
128171
/**
129172
* Get all descendants (query version)
130173
*
131-
* @param string|int|Model|NestedSetTrait $id
174+
* @param string|int|Model|UseNestedSet $id
132175
* @param string $boolean
133176
* @param bool $not
134177
* @param bool $andSelf
@@ -282,7 +325,10 @@ public function leaf(): static
282325
public function defaultOrder(int $dir = SORT_ASC): static
283326
{
284327
$this->query->orders = null;
285-
$this->query->orderBy((string)$this->model->leftAttribute(), $dir === SORT_ASC ? 'asc' : 'desc');
328+
$this->query->orderBy(
329+
$this->columnWithTbl((string)$this->model->leftAttribute()),
330+
$dir === SORT_ASC ? 'asc' : 'desc'
331+
);
286332

287333
return $this;
288334
}
@@ -331,7 +377,7 @@ public function whereNodeBetween(array $values, string $boolean = 'and', bool $n
331377

332378
$this->query
333379
->whereBetween(
334-
"{$this->model->getTable()}.{$this->model->leftAttribute()}",
380+
$this->columnWithTbl((string)$this->model->leftAttribute()),
335381
[
336382
$left,
337383
$right,
@@ -342,7 +388,7 @@ public function whereNodeBetween(array $values, string $boolean = 'and', bool $n
342388

343389
if ($this->model->isMulti()) {
344390
$treeId = end($values);
345-
$this->query->where("{$this->model->getTable()}.{$this->model->treeAttribute()}", $treeId);
391+
$this->query->where($this->columnWithTbl((string)$this->model->treeAttribute()), $treeId);
346392
}
347393

348394
return $this;
@@ -427,4 +473,9 @@ public function wrappedKey(): string
427473
{
428474
return $this->query->getGrammar()->wrap($this->model->getKeyName());
429475
}
476+
477+
protected function columnWithTbl(string $column): string
478+
{
479+
return $this->model->getTable() . '.' . $column;
480+
}
430481
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Fureev\Trees\Tests\Functional\Tree\Multi\QueryBuilder;
6+
7+
use Fureev\Trees\Tests\Functional\AbstractFunctionalTreeTestCase;
8+
use Fureev\Trees\Tests\models\v5\MultiCategory;
9+
use PHPUnit\Framework\Attributes\Test;
10+
11+
class ParentsByModelIdTest extends AbstractFunctionalTreeTestCase
12+
{
13+
/**
14+
* @return class-string<MultiCategory>
15+
*/
16+
protected static function modelClass(): string
17+
{
18+
return MultiCategory::class;
19+
}
20+
21+
#[Test]
22+
public function basic(): void
23+
{
24+
/** @var MultiCategory $modelRoot */
25+
$modelRoot = static::model(['title' => 'root node']);
26+
$modelRoot->save();
27+
28+
$treeId = $modelRoot->tree_id;
29+
static::assertNotNull($treeId);
30+
31+
// Level 2
32+
/** @var MultiCategory $node21 */
33+
$node21 = static::model(['title' => 'child 2.1']);
34+
$node21->appendTo($modelRoot)->save();
35+
$modelRoot->refresh();
36+
37+
// Level 3
38+
/** @var MultiCategory $node31 */
39+
$node31 = static::model(['title' => 'child 3.1']);
40+
$node31->appendTo($node21)->save();
41+
$modelRoot->refresh();
42+
43+
$collection = MultiCategory::parentsByModelId($node31->id)->get();
44+
45+
self::assertCount(2, $collection);
46+
}
47+
48+
#[Test]
49+
public function andSelf(): void
50+
{
51+
/** @var MultiCategory $modelRoot */
52+
$modelRoot = static::model(['title' => 'root node']);
53+
$modelRoot->save();
54+
55+
$treeId = $modelRoot->tree_id;
56+
static::assertNotNull($treeId);
57+
58+
// Level 2
59+
/** @var MultiCategory $node21 */
60+
$node21 = static::model(['title' => 'child 2.1']);
61+
$node21->appendTo($modelRoot)->save();
62+
$modelRoot->refresh();
63+
64+
// Level 3
65+
/** @var MultiCategory $node31 */
66+
$node31 = static::model(['title' => 'child 3.1']);
67+
$node31->appendTo($node21)->save();
68+
$modelRoot->refresh();
69+
70+
///
71+
72+
/** @var MultiCategory $modelRoot2 */
73+
$modelRoot2 = static::model(['title' => 'root node 2']);
74+
$modelRoot2->save();
75+
76+
// Level 2
77+
/** @var MultiCategory $node221 */
78+
$node221 = static::model(['title' => 'child 22.1']);
79+
$node221->appendTo($modelRoot2)->save();
80+
$modelRoot2->refresh();
81+
82+
// Level 3
83+
/** @var MultiCategory $node231 */
84+
$node231 = static::model(['title' => 'child 23.1']);
85+
$node231->appendTo($node221)->save();
86+
$modelRoot2->refresh();
87+
88+
$collection = MultiCategory::parentsByModelId($node31->id, andSelf: true)->get();
89+
self::assertCount(3, $collection);
90+
91+
$collection = MultiCategory::parentsByModelId($node31->id, 2)->get();
92+
self::assertCount(0, $collection);
93+
94+
$collection = MultiCategory::parentsByModelId($node31->id, 1)->get();
95+
self::assertCount(1, $collection);
96+
97+
$collection = MultiCategory::parentsByModelId($node31->id, 1, true)->get();
98+
self::assertCount(2, $collection);
99+
100+
$collection = MultiCategory::parentsByModelId($node231->id)->get();
101+
self::assertCount(2, $collection);
102+
}
103+
}

0 commit comments

Comments
 (0)