Skip to content

[Feature] Support encoded resource ids #55

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"laravel/framework": "^8.0"
},
"require-dev": {
"laravel-json-api/hashids": "dev-develop",
"laravel-json-api/testing": "^1.0.0-alpha.1",
"orchestra/testbench": "^6.9",
"phpunit/phpunit": "^9.5"
Expand Down
54 changes: 54 additions & 0 deletions src/Routing/Route.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,13 +210,43 @@ public function relation(): Relation
*/
public function substituteBindings(): void
{
if ($this->hasSubstitutedBindings()) {
$this->checkBinding();
return;
}

if ($this->hasResourceId()) {
$this->setModel($this->schema()->repository()->find(
$this->resourceId()
));
}
}

/**
* Has the model binding already been substituted?
*
* In a normal Laravel application setup, the `api` middleware group will
* include Laravel's binding substitution middleware. This means that
* typically the boot JSON:API middleware will run *after* bindings have been
* substituted.
*
* If the route that is being executed has type-hinted the model, this means
* the model will already be substituted into the route. For example, this
* can occur if the developer has written their own controller action, or
* for custom actions.
*
* @return bool
*/
private function hasSubstitutedBindings(): bool
{
if ($name = $this->resourceIdName()) {
$expected = $this->schema()->model();
return $this->route->parameter($name) instanceof $expected;
}

return false;
}

/**
* @param object|null $model
* @return void
Expand All @@ -235,6 +265,30 @@ private function setModel(?object $model): void
throw new NotFoundHttpException();
}

/**
* Check the model that has already been substituted.
*
* If Laravel has substituted bindings before the JSON:API binding substitution
* is triggered, we need to check that the model that has been set on the route
* by Laravel does exist in our API. This is because the API's existence logic
* may not match the route binding query that Laravel executed to substitute
* the binding. E.g. if the developer has applied global scopes in the Server's
* `serving()` method, these global scopes may have been applied *after* the
* binding was substituted.
*
* @return void
*/
private function checkBinding(): void
{
$resourceId = $this->server->resources()->create(
$this->model(),
)->id();

if (!$this->schema()->repository()->exists($resourceId)) {
throw new NotFoundHttpException();
}
}

/**
* @return string|null
*/
Expand Down
4 changes: 2 additions & 2 deletions stubs/schema.stub
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use {{ namespacedModel }};
use LaravelJsonApi\Eloquent\Contracts\Paginator;
use LaravelJsonApi\Eloquent\Fields\DateTime;
use LaravelJsonApi\Eloquent\Fields\ID;
use LaravelJsonApi\Eloquent\Filters\WhereIn;
use LaravelJsonApi\Eloquent\Filters\WhereIdIn;
use LaravelJsonApi\Eloquent\Pagination\PagePagination;
use LaravelJsonApi\Eloquent\{{ schema }};

Expand Down Expand Up @@ -42,7 +42,7 @@ class {{ class }} extends {{ schema }}
public function filters(): array
{
return [
WhereIn::make('id', $this->idColumn()),
WhereIdIn::make($this),
];
}

Expand Down
7 changes: 4 additions & 3 deletions tests/dummy/app/JsonApi/V1/Comments/CommentSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@
use LaravelJsonApi\Eloquent\Fields\ID;
use LaravelJsonApi\Eloquent\Fields\Relations\BelongsTo;
use LaravelJsonApi\Eloquent\Fields\Str;
use LaravelJsonApi\Eloquent\Filters\WhereIn;
use LaravelJsonApi\Eloquent\Filters\WhereIdIn;
use LaravelJsonApi\Eloquent\Pagination\PagePagination;
use LaravelJsonApi\Eloquent\Schema;
use LaravelJsonApi\HashIds\HashId;

class CommentSchema extends Schema
{
Expand All @@ -45,7 +46,7 @@ class CommentSchema extends Schema
public function fields(): array
{
return [
ID::make(),
HashId::make()->alreadyEncoded(),
Str::make('content'),
DateTime::make('createdAt')->sortable()->readOnly(),
BelongsTo::make('post'),
Expand All @@ -60,7 +61,7 @@ public function fields(): array
public function filters(): array
{
return [
WhereIn::make('id', $this->idColumn())->delimiter(','),
WhereIdIn::make($this)->delimiter(','),
];
}

Expand Down
4 changes: 2 additions & 2 deletions tests/dummy/app/JsonApi/V1/Images/ImageSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
use LaravelJsonApi\Eloquent\Fields\DateTime;
use LaravelJsonApi\Eloquent\Fields\ID;
use LaravelJsonApi\Eloquent\Fields\Str;
use LaravelJsonApi\Eloquent\Filters\WhereIn;
use LaravelJsonApi\Eloquent\Filters\WhereIdIn;
use LaravelJsonApi\Eloquent\Pagination\PagePagination;
use LaravelJsonApi\Eloquent\Schema;

Expand Down Expand Up @@ -57,7 +57,7 @@ public function fields(): array
public function filters(): array
{
return [
WhereIn::make('id', $this->idColumn())->delimiter(','),
WhereIdIn::make($this)->delimiter(','),
];
}

Expand Down
8 changes: 4 additions & 4 deletions tests/dummy/app/JsonApi/V1/Posts/PostSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
use App\Models\Post;
use LaravelJsonApi\Eloquent\Contracts\Paginator;
use LaravelJsonApi\Eloquent\Fields\DateTime;
use LaravelJsonApi\Eloquent\Fields\ID;
use LaravelJsonApi\Eloquent\Fields\Relations\BelongsTo;
use LaravelJsonApi\Eloquent\Fields\Relations\BelongsToMany;
use LaravelJsonApi\Eloquent\Fields\Relations\HasMany;
Expand All @@ -32,10 +31,11 @@
use LaravelJsonApi\Eloquent\Filters\OnlyTrashed;
use LaravelJsonApi\Eloquent\Filters\Scope;
use LaravelJsonApi\Eloquent\Filters\Where;
use LaravelJsonApi\Eloquent\Filters\WhereIn;
use LaravelJsonApi\Eloquent\Filters\WhereIdIn;
use LaravelJsonApi\Eloquent\Pagination\PagePagination;
use LaravelJsonApi\Eloquent\Schema;
use LaravelJsonApi\Eloquent\SoftDeletes;
use LaravelJsonApi\HashIds\HashId;

class PostSchema extends Schema
{
Expand All @@ -62,7 +62,7 @@ class PostSchema extends Schema
public function fields(): array
{
return [
ID::make(),
HashId::make()->alreadyEncoded(),
BelongsTo::make('author')->type('users')->readOnly(),
HasMany::make('comments')->readOnly(),
Str::make('content'),
Expand All @@ -87,7 +87,7 @@ public function fields(): array
public function filters(): array
{
return [
WhereIn::make('id', $this->idColumn())->delimiter(','),
WhereIdIn::make($this)->delimiter(','),
Scope::make('published', 'wherePublished')->asBoolean(),
Where::make('slug')->singular(),
OnlyTrashed::make('trashed'),
Expand Down
8 changes: 4 additions & 4 deletions tests/dummy/app/JsonApi/V1/Tags/TagSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@
use App\Models\Tag;
use LaravelJsonApi\Eloquent\Contracts\Paginator;
use LaravelJsonApi\Eloquent\Fields\DateTime;
use LaravelJsonApi\Eloquent\Fields\ID;
use LaravelJsonApi\Eloquent\Fields\Relations\BelongsToMany;
use LaravelJsonApi\Eloquent\Fields\Str;
use LaravelJsonApi\Eloquent\Filters\WhereIn;
use LaravelJsonApi\Eloquent\Filters\WhereIdIn;
use LaravelJsonApi\Eloquent\Pagination\PagePagination;
use LaravelJsonApi\Eloquent\Schema;
use LaravelJsonApi\HashIds\HashId;

class TagSchema extends Schema
{
Expand All @@ -45,7 +45,7 @@ class TagSchema extends Schema
public function fields(): array
{
return [
ID::make(),
HashId::make()->alreadyEncoded(),
DateTime::make('createdAt')->sortable()->readOnly(),
Str::make('name')->sortable(),
BelongsToMany::make('posts')
Expand All @@ -64,7 +64,7 @@ public function fields(): array
public function filters(): array
{
return [
WhereIn::make('id', $this->idColumn())->delimiter(','),
WhereIdIn::make($this)->delimiter(','),
];
}

Expand Down
8 changes: 4 additions & 4 deletions tests/dummy/app/JsonApi/V1/Users/UserSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@
use App\Models\User;
use LaravelJsonApi\Eloquent\Contracts\Paginator;
use LaravelJsonApi\Eloquent\Fields\DateTime;
use LaravelJsonApi\Eloquent\Fields\ID;
use LaravelJsonApi\Eloquent\Fields\Str;
use LaravelJsonApi\Eloquent\Filters\Where;
use LaravelJsonApi\Eloquent\Filters\WhereIn;
use LaravelJsonApi\Eloquent\Filters\WhereIdIn;
use LaravelJsonApi\Eloquent\Pagination\PagePagination;
use LaravelJsonApi\Eloquent\Schema;
use LaravelJsonApi\HashIds\HashId;

class UserSchema extends Schema
{
Expand All @@ -45,7 +45,7 @@ class UserSchema extends Schema
public function fields(): array
{
return [
ID::make(),
HashId::make()->alreadyEncoded(),
DateTime::make('createdAt')->readOnly(),
Str::make('name'),
DateTime::make('updatedAt')->readOnly(),
Expand All @@ -58,7 +58,7 @@ public function fields(): array
public function filters(): array
{
return [
WhereIn::make('id', $this->idColumn())->delimiter(','),
WhereIdIn::make($this)->delimiter(','),
Where::make('email')->singular(),
];
}
Expand Down
4 changes: 2 additions & 2 deletions tests/dummy/app/JsonApi/V1/Videos/VideoSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
use LaravelJsonApi\Eloquent\Fields\ID;
use LaravelJsonApi\Eloquent\Fields\Relations\BelongsToMany;
use LaravelJsonApi\Eloquent\Fields\Str;
use LaravelJsonApi\Eloquent\Filters\WhereIn;
use LaravelJsonApi\Eloquent\Filters\WhereIdIn;
use LaravelJsonApi\Eloquent\Pagination\PagePagination;
use LaravelJsonApi\Eloquent\Schema;

Expand Down Expand Up @@ -60,7 +60,7 @@ public function fields(): array
public function filters(): array
{
return [
WhereIn::make('id', $this->idColumn())->delimiter(','),
WhereIdIn::make($this)->delimiter(','),
];
}

Expand Down
1 change: 1 addition & 0 deletions tests/dummy/app/Models/Comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class Comment extends Model
{

use HasFactory;
use Concerns\HashRouteKey;

/**
* @var string[]
Expand Down
60 changes: 60 additions & 0 deletions tests/dummy/app/Models/Concerns/HashRouteKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php
/*
* Copyright 2021 Cloud Creativity Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

declare(strict_types=1);

namespace App\Models\Concerns;

use Vinkla\Hashids\Facades\Hashids;

trait HashRouteKey
{

/**
* Get the value of the model's route key.
*
* @return string
*/
public function getRouteKey()
{
return Hashids::encode(
$this->getAttribute($this->getRouteKeyName())
);
}

/**
* Retrieve the model for a bound value.
*
* @param mixed $value
* @param string|null $field
* @return \Illuminate\Database\Eloquent\Model|null
*/
public function resolveRouteBinding($value, $field = null)
{
if (empty($field) || $field === $this->getRouteKeyName()) {
return $this->newQuery()->where(
$field ?: $this->getRouteKeyName(),
Hashids::decode($value)
)->firstOrFail();
}

return $this
->newQuery()
->where($field, $value)
->firstOrFail();
}
}
1 change: 1 addition & 0 deletions tests/dummy/app/Models/Post.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class Post extends Model

use HasFactory;
use SoftDeletes;
use Concerns\HashRouteKey;

/**
* @var string[]
Expand Down
1 change: 1 addition & 0 deletions tests/dummy/app/Models/Tag.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class Tag extends Model
{

use HasFactory;
use Concerns\HashRouteKey;

/**
* @var string[]
Expand Down
1 change: 1 addition & 0 deletions tests/dummy/app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class User extends Authenticatable

use HasFactory;
use Notifiable;
use Concerns\HashRouteKey;

/**
* The attributes that are mass assignable.
Expand Down
Loading