Skip to content

Commit

Permalink
fix: Sorting by has one (#437)
Browse files Browse the repository at this point in the history
* fix: Sorting by has one

* Fix styling

Co-authored-by: binaryk <[email protected]>
  • Loading branch information
binaryk and binaryk authored Nov 18, 2021
1 parent a89864b commit a26b5dd
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 31 deletions.
80 changes: 70 additions & 10 deletions docs-v2/content/en/search/sorting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'),
];
}
```

<alert>

The `sortable` method accepts the column (or fully qualified column name) of the related model.

</alert>


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
Expand All @@ -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),
)
];
Expand All @@ -74,7 +134,7 @@ As you may notice we have typed twice the `users.name` (on the array key, and as

</alert>

### Sort using closure
## Sort using closure

If you have a quick sort method, you can use a closure to sort your data:

Expand All @@ -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:

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/4.0/filtering/filtering.md
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)
];
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/5.0/filtering/filtering.md
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)
];
Expand Down
5 changes: 3 additions & 2 deletions src/Eager/RelatedCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 5 additions & 1 deletion src/Fields/HasOne.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
1 change: 0 additions & 1 deletion src/Filters/SortCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
45 changes: 32 additions & 13 deletions src/Filters/SortableFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -17,7 +18,7 @@ class SortableFilter extends Filter

public string $direction = 'asc';

private BelongsTo $belongsToField;
private HasOne|BelongsTo $relation;

private Closure $resolver;

Expand All @@ -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),
Expand All @@ -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;
}
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
2 changes: 1 addition & 1 deletion tests/Feature/Filters/BelongsToFilterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
),
];
Expand Down
41 changes: 41 additions & 0 deletions tests/Fields/HasOneFieldTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion tests/Fixtures/User/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down

0 comments on commit a26b5dd

Please sign in to comment.