Skip to content

Commit

Permalink
Add Bloodline relation
Browse files Browse the repository at this point in the history
  • Loading branch information
staudenmeir committed Nov 13, 2021
1 parent 0345b92 commit 76823e6
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 10 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ 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.
Expand Down Expand Up @@ -246,7 +247,7 @@ $descendants = User::find($id)->descendants()->depthFirst()->get();

### Depth

The results of ancestor, descendant and tree queries include an additional `depth` column.
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:
Expand Down Expand Up @@ -295,7 +296,7 @@ $tree = User::treeOf($constraint, 3)->get();

### Path

The results of ancestor, descendant and tree queries include an additional `path` column.
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:

Expand Down
45 changes: 37 additions & 8 deletions src/Eloquent/HasRecursiveRelationshipScopes.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Staudenmeir\LaravelAdjacencyList\Eloquent;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
use Staudenmeir\LaravelAdjacencyList\Query\Grammars\ExpressionGrammar;

trait HasRecursiveRelationshipScopes
Expand Down Expand Up @@ -206,7 +207,22 @@ protected function getRecursiveQuery(ExpressionGrammar $grammar, $direction, $fr

$depth = $grammar->wrap($this->getDepthName());

$recursiveDepth = $grammar->wrap($this->getDepthName()).' '.($direction === 'asc' ? '-' : '+').' 1';
$joinColumns = [
'asc' => [
$name.'.'.$this->getParentKeyName(),
$this->getQualifiedLocalKeyName(),
],
'desc' => [
$name.'.'.$this->getLocalKeyName(),
$this->qualifyColumn($this->getParentKeyName()),
],
];

if ($direction === 'both') {
$recursiveDepth = "$depth + (case when {$joinColumns['desc'][1]}={$joinColumns['desc'][0]} then 1 else -1 end)";
} else {
$recursiveDepth = $depth.' '.($direction === 'asc' ? '-' : '+').' 1';
}

$recursivePath = $grammar->compileRecursivePath(
$this->getQualifiedLocalKeyName(),
Expand All @@ -231,16 +247,29 @@ protected function getRecursiveQuery(ExpressionGrammar $grammar, $direction, $fr
);
}

if ($direction === 'asc') {
$first = $this->getParentKeyName();
$second = $this->getQualifiedLocalKeyName();
if ($direction === 'both') {
$query->join($name, function (JoinClause $join) use ($joinColumns) {
$join->on($joinColumns['asc'][0], '=', $joinColumns['asc'][1])
->orOn($joinColumns['desc'][0], '=', $joinColumns['desc'][1]);
});

$depth = $this->getDepthName();

$query->where(function (Builder $query) use ($depth, $joinColumns) {
$query->where($depth, '=', 0)
->orWhere(function (Builder $query) use ($depth, $joinColumns) {
$query->whereColumn($joinColumns['asc'][0], '=', $joinColumns['asc'][1])
->where($depth, '<', 0);
})
->orWhere(function (Builder $query) use ($depth, $joinColumns) {
$query->whereColumn($joinColumns['desc'][0], '=', $joinColumns['desc'][1])
->where($depth, '>', 0);
});
});
} else {
$first = $this->getLocalKeyName();
$second = $this->qualifyColumn($this->getParentKeyName());
$query->join($name, $joinColumns[$direction][0], '=', $joinColumns[$direction][1]);
}

$query->join($name, $name.'.'.$first, '=', $second);

if (!is_null($maxDepth)) {
$query->where($this->getDepthName(), '<', $maxDepth);
}
Expand Down
30 changes: 30 additions & 0 deletions src/Eloquent/HasRecursiveRelationships.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Staudenmeir\LaravelAdjacencyList\Eloquent\Relations\Ancestors;
use Staudenmeir\LaravelAdjacencyList\Eloquent\Relations\Bloodline;
use Staudenmeir\LaravelAdjacencyList\Eloquent\Relations\Descendants;
use Staudenmeir\LaravelAdjacencyList\Eloquent\Relations\HasManyOfDescendants;
use Staudenmeir\LaravelAdjacencyList\Eloquent\Relations\RootAncestor;
Expand Down Expand Up @@ -154,6 +155,35 @@ protected function newAncestors(Builder $query, Model $parent, $foreignKey, $loc
return new Ancestors($query, $parent, $foreignKey, $localKey, $andSelf);
}

/**
* Get the model's bloodline.
*
* @return \Staudenmeir\LaravelAdjacencyList\Eloquent\Relations\Bloodline
*/
public function bloodline()
{
return $this->newBloodline(
(new static())->newQuery(),
$this,
$this->getQualifiedParentKeyName(),
$this->getLocalKeyName()
);
}

/**
* Instantiate a new Bloodline relationship.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Model $parent
* @param string $foreignKey
* @param string $localKey
* @return \Staudenmeir\LaravelAdjacencyList\Eloquent\Relations\Bloodline
*/
protected function newBloodline(Builder $query, Model $parent, $foreignKey, $localKey)
{
return new Bloodline($query, $parent, $foreignKey, $localKey);
}

/**
* Get the model's children.
*
Expand Down
38 changes: 38 additions & 0 deletions src/Eloquent/Relations/Bloodline.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace Staudenmeir\LaravelAdjacencyList\Eloquent\Relations;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class Bloodline extends Descendants
{
/**
* Create a new bloodline relationship instance.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Model $parent
* @param string $foreignKey
* @param string $localKey
* @return void
*/
public function __construct(Builder $query, Model $parent, $foreignKey, $localKey)
{
parent::__construct($query, $parent, $foreignKey, $localKey, true);
}

/**
* Add a recursive expression to the query.
*
* @param callable $constraint
* @param \Illuminate\Database\Eloquent\Builder|null $query
* @param string|null $from
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function addExpression(callable $constraint, Builder $query = null, $from = null)
{
$query = $query ?: $this->query;

return $query->withRelationshipExpression('both', $constraint, 0, $from);
}
}
86 changes: 86 additions & 0 deletions tests/BloodlineTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

namespace Tests;

use Illuminate\Database\Capsule\Manager as DB;
use Staudenmeir\LaravelAdjacencyList\Eloquent\Relations\Bloodline;
use Tests\Models\User;

class BloodlineTest extends TestCase
{
public function testLazyLoading()
{
$bloodline = User::find(5)->bloodline()->breadthFirst()->get();

$this->assertEquals([1, 2, 5, 8], $bloodline->pluck('id')->all());
$this->assertEquals([-2, -1, 0, 1], $bloodline->pluck('depth')->all());
$this->assertEquals(['5.2.1', '5.2', '5', '5.8'], $bloodline->pluck('path')->all());
$this->assertEquals(['user-5/user-2/user-1', 'user-5/user-2', 'user-5', 'user-5/user-8'], $bloodline->pluck('slug_path')->all());
}

public function testEagerLoading()
{
$users = User::with(
[
'bloodline' => function (Bloodline $relation) {
$relation->breadthFirst()->orderBy('id');
},
]
)->get();

$this->assertEquals(range(1, 9), $users[0]->bloodline->pluck('id')->all());
$this->assertEquals([1, 2, 5, 8], $users[1]->bloodline->pluck('id')->all());
$this->assertEquals([1, 2, 5, 8], $users[4]->bloodline->pluck('id')->all());
$this->assertEquals(['5.2.1', '5.2', '5', '5.8'], $users[4]->bloodline->pluck('path')->all());
$this->assertEquals(['user-5/user-2/user-1', 'user-5/user-2', 'user-5', 'user-5/user-8'], $users[4]->bloodline->pluck('slug_path')->all());
}

public function testLazyEagerLoading()
{
$users = User::all()->load(
[
'bloodline' => function (Bloodline $relation) {
$relation->breadthFirst()->orderBy('id');
},
]
);

$this->assertEquals(range(1, 9), $users[0]->bloodline->pluck('id')->all());
$this->assertEquals([1, 2, 5, 8], $users[1]->bloodline->pluck('id')->all());
$this->assertEquals([1, 2, 5, 8], $users[4]->bloodline->pluck('id')->all());
$this->assertEquals(['5.2.1', '5.2', '5', '5.8'], $users[4]->bloodline->pluck('path')->all());
$this->assertEquals(['user-5/user-2/user-1', 'user-5/user-2', 'user-5', 'user-5/user-8'], $users[4]->bloodline->pluck('slug_path')->all());
}

public function testExistenceQuery()
{
if (DB::connection()->getDriverName() === 'sqlsrv') {
$this->markTestSkipped();
}

$descendants = User::first()->descendants()->has('bloodline', '<', 4)->get();

$this->assertEquals([4, 7], $descendants->pluck('id')->all());
}

public function testExistenceQueryForSelfRelation()
{
if (DB::connection()->getDriverName() === 'sqlsrv') {
$this->markTestSkipped();
}

$users = User::has('bloodline', '<', 4)->get();

$this->assertEquals([4, 7, 11, 12], $users->pluck('id')->all());
}

public function testUpdate()
{
$affected = User::find(5)->bloodline()->delete();

$this->assertEquals(4, $affected);
$this->assertNotNull(User::withTrashed()->find(1)->deleted_at);
$this->assertNotNull(User::withTrashed()->find(8)->deleted_at);
$this->assertNull(User::find(3)->deleted_at);
}
}

0 comments on commit 76823e6

Please sign in to comment.