From a26b5dd7bf345bc968adc6636cd9eb6dfa82d84e Mon Sep 17 00:00:00 2001 From: Lupacescu Eduard Date: Thu, 18 Nov 2021 16:15:40 +0200 Subject: [PATCH] fix: Sorting by has one (#437) * fix: Sorting by has one * Fix styling Co-authored-by: binaryk --- docs-v2/content/en/search/sorting.md | 80 ++++++++++++++++--- docs/docs/4.0/filtering/filtering.md | 2 +- docs/docs/5.0/filtering/filtering.md | 2 +- src/Eager/RelatedCollection.php | 5 +- src/Fields/HasOne.php | 6 +- src/Filters/SortCollection.php | 1 - src/Filters/SortableFilter.php | 45 ++++++++--- tests/Feature/Filters/BelongsToFilterTest.php | 2 +- tests/Fields/HasOneFieldTest.php | 41 ++++++++++ tests/Fixtures/User/User.php | 3 +- 10 files changed, 156 insertions(+), 31 deletions(-) diff --git a/docs-v2/content/en/search/sorting.md b/docs-v2/content/en/search/sorting.md index 41495401..052c07e9 100644 --- a/docs-v2/content/en/search/sorting.md +++ b/docs-v2/content/en/search/sorting.md @@ -6,6 +6,8 @@ category: Search & Filters position: 13 --- + ## Definition + During index requests, usually we have to sort by specific attributes. This requires the `$sort` configuration: ```php @@ -16,30 +18,88 @@ class PostRepository extends Repository Performing request requires the sort query param: -### Descending sorting +## Descending sorting Sorting DESC requires a minus (`-`) sign before the attribute name: - ```http request + ```http_request GET: /api/restify/posts?sort=-id ``` Sorting ASC: - ```http request + ```http_request GET: /api/restify/posts?sort=id ``` or with plus sign before the field: - ```http request + ```http_request GET: /api/restify/posts?sort=+id ``` -### Sort using BelongsTo +## Sort using relation + +Sometimes you may need to sort by a `belongsTo` or `hasOne` relationship. + +This become a breeze with Restify. Firstly you have to instruct your sort to use a relationship: + +### HasOne sorting + +Using a `related` relationship, it becomes very easy to define a sortable by has one related. + +You simply add the `->sortable()` method to the relationship: + +```php +// UserRepository.php + +public function related(): array +{ + return [ + 'post' => HasOne::make('post', PostRepository::class)->sortable('title'), + ]; +} +``` + + + +The `sortable` method accepts the column (or fully qualified column name) of the related model. + + + + +The API request will always have to use the full path to the `attributes`: + +```bash +GET: /api/restify/posts?sort=post.attributes.title +``` + +The structure of the `sort` query param value consist always from 3 parts: + +- `post` - the name of the relation defined in the `related` method +- `attributes` - a generic json:api term +- `title` - the column name from the database of the related model + +### BelongsTo sorting + +The belongsTo sorting works in a similar way. + +You simply add the `->sortable()` method to the relationship: + +```php +// PostRepository.php + +public function related(): array +{ + return [ + 'user' => BelongsTo::make('user', UserRepository::class)->sortable('name'), + ]; +} +``` + +### Using custom sortable filter -Sometimes you may need to sort by a `belongsTo` relationship. This become a breeze with Restify. Firstly you have to -instruct your sort to use a relationship: +You can override the `sorts` method, and return an instance of `SortableFilter` that might be instructed to use a relationship: ```php // PostRepository @@ -51,7 +111,7 @@ public static function sorts(): array return [ 'users.name' => SortableFilter::make() ->setColumn('users.name') - ->usingBelongsTo( + ->usingRelation( BelongsTo::make('user', 'user', UserRepository::class), ) ]; @@ -74,7 +134,7 @@ As you may notice we have typed twice the `users.name` (on the array key, and as -### Sort using closure +## Sort using closure If you have a quick sort method, you can use a closure to sort your data: @@ -92,7 +152,7 @@ public static function sorts(): array } ``` -### Get available sorts +## Get available sorts You can use the following request to get sortable attributes for a repository: diff --git a/docs/docs/4.0/filtering/filtering.md b/docs/docs/4.0/filtering/filtering.md index f34e2952..e5951cde 100644 --- a/docs/docs/4.0/filtering/filtering.md +++ b/docs/docs/4.0/filtering/filtering.md @@ -251,7 +251,7 @@ public static function sorts(): array return [ 'users.name' => SortableFilter::make() ->setColumn('users.name') - ->usingBelongsTo( + ->usingRelation( BelongsTo::make('user', 'user', UserRepository::class), ) ]; diff --git a/docs/docs/5.0/filtering/filtering.md b/docs/docs/5.0/filtering/filtering.md index d49d4807..4515493a 100644 --- a/docs/docs/5.0/filtering/filtering.md +++ b/docs/docs/5.0/filtering/filtering.md @@ -251,7 +251,7 @@ public static function sorts(): array return [ 'users.name' => SortableFilter::make() ->setColumn('users.name') - ->usingBelongsTo( + ->usingRelation( BelongsTo::make('user', 'user', UserRepository::class), ) ]; diff --git a/src/Eager/RelatedCollection.php b/src/Eager/RelatedCollection.php index 207342da..02e6ec96 100644 --- a/src/Eager/RelatedCollection.php +++ b/src/Eager/RelatedCollection.php @@ -7,6 +7,7 @@ use Binaryk\LaravelRestify\Fields\Contracts\Sortable; use Binaryk\LaravelRestify\Fields\EagerField; use Binaryk\LaravelRestify\Fields\Field; +use Binaryk\LaravelRestify\Fields\HasOne; use Binaryk\LaravelRestify\Fields\MorphToMany; use Binaryk\LaravelRestify\Filters\SortableFilter; use Binaryk\LaravelRestify\Http\Requests\RestifyRequest; @@ -59,8 +60,8 @@ public function mapIntoSortable(): self ->map(function (Sortable $field) { $filter = SortableFilter::make(); - if ($field instanceof BelongsTo) { - return $filter->usingBelongsTo($field)->setColumn($field->qualifySortable()); + if ($field instanceof BelongsTo || $field instanceof HasOne) { + return $filter->usingRelation($field)->setColumn($field->qualifySortable()); } return null; diff --git a/src/Fields/HasOne.php b/src/Fields/HasOne.php index ed554e56..9c151e37 100644 --- a/src/Fields/HasOne.php +++ b/src/Fields/HasOne.php @@ -2,11 +2,15 @@ namespace Binaryk\LaravelRestify\Fields; +use Binaryk\LaravelRestify\Fields\Concerns\CanSort; +use Binaryk\LaravelRestify\Fields\Contracts\Sortable; use Binaryk\LaravelRestify\Http\Requests\RestifyRequest; use Binaryk\LaravelRestify\Repositories\Repository; -class HasOne extends EagerField +class HasOne extends EagerField implements Sortable { + use CanSort; + public function __construct($relation, $parentRepository) { if (! is_a(app($parentRepository), Repository::class)) { diff --git a/src/Filters/SortCollection.php b/src/Filters/SortCollection.php index 9b903079..b4584fc6 100644 --- a/src/Filters/SortCollection.php +++ b/src/Filters/SortCollection.php @@ -68,7 +68,6 @@ public function hydrateDefinition(Repository $repository): SortCollection return $relatedSortableFilter->syncDirection($filter->direction()); } - if (! array_key_exists($filter->column, $repository::sorts())) { return $filter; } diff --git a/src/Filters/SortableFilter.php b/src/Filters/SortableFilter.php index cb37060a..ce90c244 100644 --- a/src/Filters/SortableFilter.php +++ b/src/Filters/SortableFilter.php @@ -5,6 +5,7 @@ use Binaryk\LaravelRestify\Fields\BelongsTo; use Binaryk\LaravelRestify\Fields\Contracts\Sortable; use Binaryk\LaravelRestify\Fields\EagerField; +use Binaryk\LaravelRestify\Fields\HasOne; use Binaryk\LaravelRestify\Http\Requests\RestifyRequest; use Closure; use Illuminate\Database\Eloquent\Builder; @@ -17,7 +18,7 @@ class SortableFilter extends Filter public string $direction = 'asc'; - private BelongsTo $belongsToField; + private HasOne|BelongsTo $relation; private Closure $resolver; @@ -29,23 +30,23 @@ class SortableFilter extends Filter * @param string $value * @return Builder */ - public function filter(RestifyRequest $request, Builder | Relation $query, $value) + public function filter(RestifyRequest $request, Builder|Relation $query, $value) { if (isset($this->resolver) && is_callable($this->resolver)) { return call_user_func($this->resolver, $request, $query, $value); } - if (isset($this->belongsToField)) { - if (! $this->belongsToField->authorize($request)) { + if (isset($this->relation)) { + if (! $this->relation->authorize($request)) { return $query; } // This approach could be rewritten using join. $query->orderBy( - $this->belongsToField->getRelatedModel($this->repository)::select($this->qualifyColumn()) + $this->relation->getRelatedModel($this->repository)::select($this->qualifyColumn()) ->whereColumn( - $this->belongsToField->getQualifiedKey($this->repository), - $this->belongsToField->getRelatedKey($this->repository) + $this->relationForeignColumn(), + $this->relationRelatedColumn(), ) ->orderBy($this->qualifyColumn(), $value) ->take(1), @@ -58,9 +59,9 @@ public function filter(RestifyRequest $request, Builder | Relation $query, $valu $query->orderBy($this->column, $value); } - public function usingBelongsTo(BelongsTo $field): self + public function usingRelation(HasOne|BelongsTo $field): self { - $this->belongsToField = $field; + $this->relation = $field; return $this; } @@ -78,18 +79,18 @@ public function getSortableEager(): ?Sortable return $this->getEager(); } - public function getEager(): EagerField | Sortable | null + public function getEager(): EagerField|Sortable|null { if (! $this->hasEager()) { return null; } - return $this->belongsToField; + return $this->relation; } public function hasEager(): bool { - return isset($this->belongsToField) && $this->belongsToField instanceof EagerField; + return isset($this->relation) && $this->relation instanceof EagerField; } public function asc(): self @@ -122,7 +123,7 @@ public function resolveFrontendColumn(): self */ $tablePlural = Str::plural(Str::before($this->column, '.')); - $this->column = $tablePlural . '.' . Str::after($this->column, '.'); + $this->column = $tablePlural.'.'.Str::after($this->column, '.'); } return $this; @@ -161,4 +162,22 @@ public function usingClosure(Closure $closure): self return $this; } + + private function relationForeignColumn(): string + { + if ($this->relation instanceof HasOne) { + return $this->relation->getRelation($this->repository)->getQualifiedForeignKeyName(); + } + + return $this->relation->getQualifiedKey($this->repository); + } + + private function relationRelatedColumn(): string + { + if ($this->relation instanceof HasOne) { + return $this->relation->getRelation($this->repository)->getQualifiedParentKeyName(); + } + + return $this->relation->getRelatedKey($this->repository); + } } diff --git a/tests/Feature/Filters/BelongsToFilterTest.php b/tests/Feature/Filters/BelongsToFilterTest.php index 80a121ab..893592ee 100644 --- a/tests/Feature/Filters/BelongsToFilterTest.php +++ b/tests/Feature/Filters/BelongsToFilterTest.php @@ -19,7 +19,7 @@ public function test_can_filter_using_belongs_to_field(): void ]; PostRepository::$sort = [ - 'users.attributes.name' => SortableFilter::make()->setColumn('users.name')->usingBelongsTo( + 'users.attributes.name' => SortableFilter::make()->setColumn('users.name')->usingRelation( BelongsTo::make('user', UserRepository::class), ), ]; diff --git a/tests/Fields/HasOneFieldTest.php b/tests/Fields/HasOneFieldTest.php index cf2dd27b..915ecefc 100644 --- a/tests/Fields/HasOneFieldTest.php +++ b/tests/Fields/HasOneFieldTest.php @@ -12,8 +12,10 @@ use Binaryk\LaravelRestify\Tests\Fixtures\Post\PostPolicy; use Binaryk\LaravelRestify\Tests\Fixtures\Post\PostRepository; use Binaryk\LaravelRestify\Tests\Fixtures\User\User; +use Binaryk\LaravelRestify\Tests\Fixtures\User\UserRepository; use Binaryk\LaravelRestify\Tests\IntegrationTest; use Illuminate\Support\Facades\Gate; +use Illuminate\Testing\Fluent\AssertableJson; class HasOneFieldTest extends IntegrationTest { @@ -82,6 +84,45 @@ public function test_field_ignored_when_storing() 'post' => 'wew', ])->assertCreated(); } + + public function test_can_sort_using_has_one_to_field(): void + { + UserRepository::$related = [ + 'post' => HasOne::make('post', PostRepository::class)->sortable('posts.title'), + ]; + + Post::factory()->create([ + 'title' => 'Zez', + 'user_id' => User::factory()->create([ + 'name' => 'Last', + ]), + ]); + + Post::factory()->create([ + 'title' => 'Abc', + 'user_id' => User::factory()->create([ + 'name' => 'First', + ]), + ]); + + $this + ->getJson(UserRepository::uriKey().'?related=post&sort=-post.attributes.title&perPage=5') + ->assertJson(function (AssertableJson $assertableJson) { + $assertableJson + ->where('data.1.attributes.name', 'First') + ->where('data.0.attributes.name', 'Last') + ->etc(); + }); + + $this + ->getJson(UserRepository::uriKey().'?related=post&sort=post.attributes.title&perPage=5') + ->assertJson(function (AssertableJson $assertableJson) { + $assertableJson + ->where('data.0.attributes.name', 'First') + ->where('data.1.attributes.name', 'Last') + ->etc(); + }); + } } class UserWithPostRepository extends Repository diff --git a/tests/Fixtures/User/User.php b/tests/Fixtures/User/User.php index 04018c4b..089d74bb 100644 --- a/tests/Fixtures/User/User.php +++ b/tests/Fixtures/User/User.php @@ -9,6 +9,7 @@ use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Query\Builder; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; @@ -89,7 +90,7 @@ public function posts(): HasMany return $this->hasMany(Post::class); } - public function post() + public function post(): HasOne { return $this->hasOne(Post::class); }