Skip to content

Commit

Permalink
Refactor and document recursive query constraints
Browse files Browse the repository at this point in the history
  • Loading branch information
staudenmeir committed Jan 9, 2022
1 parent de9f28b commit b52140a
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 99 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Use this command if you are in PowerShell on Windows (e.g. in VS Code):
- [Path](#path)
- [Custom Paths](#custom-paths)
- [Nested Results](#nested-results)
- [Recursive Query Constraints](#recursive-query-constraints)
- [Custom Relationships](#custom-relationships)
- [HasManyOfDescendants](#hasmanyofdescendants)
- [BelongsToManyOfDescendants](#belongstomanyofdescendants)
Expand Down Expand Up @@ -328,6 +329,19 @@ This recursively sets `children` relationships:
]
```

### Recursive Query Constraints

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

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

### Custom Relationships

You can also define custom relationships to retrieve related models recursively.
Expand Down
17 changes: 2 additions & 15 deletions src/Eloquent/HasRecursiveRelationshipScopes.php
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,6 @@ protected function getRecursiveQuery(ExpressionGrammar $grammar, $direction, $fr
$query->where($this->getDepthName(), '<', $maxDepth);
}

$this->callRecursiveQueryDecoratingFunction($query);

return $query;
}

Expand Down Expand Up @@ -293,20 +291,9 @@ protected function addRecursiveQueryJoinsAndConstraints(Builder $query, $directi
} else {
$query->join($name, $joinColumns[$direction][0], '=', $joinColumns[$direction][1]);
}
}

/**
* Call decorating function for recursive builder if set
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function callRecursiveQueryDecoratingFunction(Builder $query) {
if (is_callable(static::$recursiveQueryDecoratingFunction)) {
(static::$recursiveQueryDecoratingFunction)($query);
static::$recursiveQueryDecoratingFunction = null;
if (static::$recursiveQueryConstraint) {
(static::$recursiveQueryConstraint)($query);
}

return $query;
}
}
82 changes: 44 additions & 38 deletions src/Eloquent/HasRecursiveRelationships.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,47 +18,12 @@ trait HasRecursiveRelationships
use HasRecursiveRelationshipScopes;
use QueriesExpressions;

/** @var null|callable */
public static $recursiveQueryDecoratingFunction;

/**
* Allows to make a query with decorating function
*
* @param callable $decoratingFunction
* @param callable $callback
* @return mixed
*/
public static function withRecursiveQueryDecoratingFunction(callable $decoratingFunction, callable $callback)
{
$previous = static::$recursiveQueryDecoratingFunction;

static::$recursiveQueryDecoratingFunction = $decoratingFunction;

$result = $callback();

static::$recursiveQueryDecoratingFunction = $previous;

return $result;
}

/**
* Set function for decorating recursive query
*
* @param callable $function
* @return void
*/
public function setRecursiveQueryDecoratingFunction(callable $function) {
static::$recursiveQueryDecoratingFunction = $function;
}

/**
* Unset function for decorating recursive query
* The additional constraint for the recursive query.
*
* @return void
* @var callable|null
*/
public function unsetRecursiveQueryDecoratingFunction() {
static::$recursiveQueryDecoratingFunction = null;
}
public static $recursiveQueryConstraint;

/**
* Get the name of the parent key column.
Expand Down Expand Up @@ -447,4 +412,45 @@ public function newCollection(array $models = [])
{
return new Collection($models);
}

/**
* Set an additional constraint for the recursive query.
*
* @param callable $constraint
* @return void
*/
public static function setRecursiveQueryConstraint(callable $constraint)
{
static::$recursiveQueryConstraint = $constraint;
}

/**
* Unset the additional constraint for the recursive query.
*
* @return void
*/
public static function unsetRecursiveQueryConstraint()
{
static::$recursiveQueryConstraint = null;
}

/**
* Execute a query with an additional constraint for the recursive query.
*
* @param callable $constraint
* @param callable $query
* @return mixed
*/
public static function withRecursiveQueryConstraint(callable $constraint, callable $query)
{
$previous = static::$recursiveQueryConstraint;

static::$recursiveQueryConstraint = $constraint;

$result = $query();

static::$recursiveQueryConstraint = $previous;

return $result;
}
}
46 changes: 0 additions & 46 deletions tests/DescendantsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,50 +142,4 @@ public function testUpdate()
$this->assertEquals(12, User::find(8)->parent_id);
$this->assertEquals(11, User::find(12)->parent_id);
}

public function testWithDecoratingFunctionForRecursiveQuery()
{
/** @var User $user */
$user = User::first();

$users = $user->descendantsAndSelf()->orderBy('id')->get();

$this->assertEquals([1, 2, 3, 4, 5, 6, 7, 8, 9], $users->pluck('id')->all());

$users = User::withRecursiveQueryDecoratingFunction(function (Builder $query) {
$query->where('users.parent_id', '<=', 3);
}, function () use ($user) {
return $user->descendantsAndSelf()->orderBy('id')->get();
});

$this->assertEquals([1, 2, 3, 4, 5, 6], $users->pluck('id')->all());

$users = $user->descendantsAndSelf()->orderBy('id')->get();

$this->assertEquals([1, 2, 3, 4, 5, 6, 7, 8, 9], $users->pluck('id')->all());
}

public function testDecoratingFunctionForRecursiveQuery()
{
/** @var User $user */
$user = User::first();

$users = $user->descendantsAndSelf()->orderBy('id')->get();

$this->assertEquals([1, 2, 3, 4, 5, 6, 7, 8, 9], $users->pluck('id')->all());

$user->setRecursiveQueryDecoratingFunction(function (Builder $query) {
$query->where('users.parent_id', '<=', 3);
});

$users = $user->descendantsAndSelf()->orderBy('id')->get();

$this->assertEquals([1, 2, 3, 4, 5, 6], $users->pluck('id')->all());

$user->unsetRecursiveQueryDecoratingFunction();

$users = $user->descendantsAndSelf()->orderBy('id')->get();

$this->assertEquals([1, 2, 3, 4, 5, 6, 7, 8, 9], $users->pluck('id')->all());
}
}
32 changes: 32 additions & 0 deletions tests/EloquentTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,36 @@ public function testScopeDepthFirstWithStringKey()

$this->assertEquals(['a', 'b', 'c', 'd'], $categories->pluck('id')->all());
}

public function testSetRecursiveQueryConstraint()
{
User::setRecursiveQueryConstraint(function (Builder $query) {
$query->where('users.parent_id', '<', 4);
});

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

$this->assertEquals([1, 2, 3, 4, 5, 6, 11], $users->pluck('id')->all());

User::unsetRecursiveQueryConstraint();

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

$this->assertEquals([1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12], $users->pluck('id')->all());
}

public function testWithRecursiveQueryConstraint()
{
$users = User::withRecursiveQueryConstraint(function (Builder $query) {
$query->where('users.parent_id', '<', 4);
}, function () {
return User::tree()->orderBy('id')->get();
});

$this->assertEquals([1, 2, 3, 4, 5, 6, 11], $users->pluck('id')->all());

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

$this->assertEquals([1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12], $users->pluck('id')->all());
}
}

0 comments on commit b52140a

Please sign in to comment.