Skip to content

Commit

Permalink
fix(core): filtering using deeply nested relations
Browse files Browse the repository at this point in the history
  • Loading branch information
alexzarbn committed Oct 13, 2023
1 parent bdc8d41 commit 6cc031f
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 5 deletions.
4 changes: 4 additions & 0 deletions src/Contracts/RelationsResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ public function __construct(array $includableRelations, array $alwaysIncludedRel

public function requestedRelations(Request $request): array;

public function relationInstanceFromParamConstraint(string $resourceModelClass, string $paramConstraint): Relation;

public function rootRelationFromParamConstraint(string $paramConstraint): string;

public function relationFromParamConstraint(string $paramConstraint): string;

public function relationFieldFromParamConstraint(string $paramConstraint): string;
Expand Down
3 changes: 1 addition & 2 deletions src/Drivers/Standard/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,7 @@ public function applyFiltersToQuery($query, Request $request, array $filterDescr
if ($relation === 'pivot') {
$this->buildPivotFilterQueryWhereClause($relationField, $filterDescriptor, $query, $or);
} else {
$relationInstance = (new $this->resourceModelClass)->{$relation}();

$relationInstance = $this->relationsResolver->relationInstanceFromParamConstraint($this->resourceModelClass, $filterDescriptor['field']);
$qualifiedRelationFieldName = $this->relationsResolver->getQualifiedRelationFieldName($relationInstance, $relationField);

$query->{$or ? 'orWhereHas' : 'whereHas'}(
Expand Down
35 changes: 34 additions & 1 deletion src/Drivers/Standard/RelationsResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
Expand Down Expand Up @@ -79,6 +81,37 @@ public function requestedRelations(Request $request): array
return $validatedIncludes;
}

public function relationInstanceFromParamConstraint(string $resourceModelClass, string $paramConstraint): Relation
{
$resourceModel = new $resourceModelClass();

do {
$relationName = $this->rootRelationFromParamConstraint($paramConstraint);
$paramConstraint = str_replace("{$relationName}.", '', $paramConstraint);

$relation = $resourceModel->{$relationName}();

if (in_array(get_class($relation), [MorphTo::class, MorphMany::class, MorphToMany::class, MorphOne::class])) {
break;
}

$resourceModel = $relation->getModel();
} while (str_contains($paramConstraint, '.'));

return $relation;
}

/**
* Resolves relation name from the given param constraint.
*
* @param string $paramConstraint
* @return string
*/
public function rootRelationFromParamConstraint(string $paramConstraint): string
{
return Arr::first(explode('.', $paramConstraint));
}

/**
* Resolves relation name from the given param constraint.
*
Expand Down Expand Up @@ -139,7 +172,7 @@ public function relationForeignKeyFromRelationInstance(Relation $relationInstanc
*/
public function getQualifiedRelationFieldName(Relation $relation, string $field): string
{
if ($relation instanceof MorphTo) {
if (in_array(get_class($relation), [MorphTo::class, MorphMany::class, MorphToMany::class, MorphOne::class])) {
return $field;
}

Expand Down
29 changes: 29 additions & 0 deletions tests/Feature/StandardIndexFilteringOperationsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,35 @@ public function getting_a_list_of_resources_filtered_by_relation_field_resources
);
}

/** @test */
public function getting_a_list_of_resources_filtered_by_deep_relation_field_resources(): void
{
$matchingPostUser = factory(User::class)->create(['name' => 'match']);
$matchingPostUser->roles()->create(['name' => 'matching-role-name']);
$matchingPost = factory(Post::class)->create(['user_id' => $matchingPostUser->id])->fresh();

$nonMatchingPostUser = factory(User::class)->create(['name' => 'not match']);
$nonMatchingPostUser->roles()->create(['name' => 'non-matching-role-name']);
factory(Post::class)->create(['user_id' => $nonMatchingPostUser->id])->fresh();

Gate::policy(Post::class, GreenPolicy::class);

$response = $this->post(
'/api/posts/search',
[
'filters' => [
['field' => 'user.name', 'operator' => '=', 'value' => 'match'],
['field' => 'user.roles.name', 'operator' => '=', 'value' => 'matching-role-name'],
],
]
);

$this->assertResourcesPaginated(
$response,
$this->makePaginator([$matchingPost], 'posts/search')
);
}

/** @test */
public function getting_a_list_of_resources_filtered_by_not_whitelisted_field(): void
{
Expand Down
1 change: 1 addition & 0 deletions tests/Fixtures/app/Http/Controllers/PostsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public function filterableBy(): array
'position',
'publish_at',
'user.name',
'user.roles.name',
'meta.name',
'meta.title',
'meta->nested_field',
Expand Down
4 changes: 2 additions & 2 deletions tests/Fixtures/app/Traits/AppliesDefaultOrder.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ protected static function boot()
parent::boot();
// Order by name ASC
static::addGlobalScope('order', function (Builder $builder) {
$builder->orderBy('id', 'asc');
$builder->orderBy($builder->getModel()->getTable().'.id', 'asc');
});
}
}
}

0 comments on commit 6cc031f

Please sign in to comment.