Skip to content

[Feature] Support polymorphic to-many relations #47

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 12, 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ All notable changes to this project will be documented in this file. This projec
full details on how to apply this to resource schemas, refer to the new *Soft Deleting* chapter in the documentation.
- Multi-resource models are now supported. This allows developers to represent a single model class as multiple
different JSON:API resource types within an API. Refer to documentation for details of how to implement.
- [#8](https://github.com/laravel-json-api/laravel/issues/8) The new `MorphToMany` relation field can now be used to add
polymorphic to-many relations to a schema. Refer to documentation for details.
- Developers can now type-hint dependencies in their server's `serving()` method.
- Can now manually register request, query and collection query classes using the `RequestResolver::registerRequest()`,
`RequestResolver::registerQuery()` and `RequestResolver::registerCollectionQuery()` static methods.

## [1.0.0-alpha.4] - 2021-02-27

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"laravel-json-api/encoder-neomerx": "^1.0.0-alpha.4",
"laravel-json-api/exceptions": "^1.0.0-alpha.2",
"laravel-json-api/spec": "^1.0.0-alpha.4",
"laravel-json-api/validation": "^1.0.0-alpha.4",
"laravel-json-api/validation": "^1.0.0-alpha.5",
"laravel/framework": "^8.0"
},
"require-dev": {
Expand Down
66 changes: 65 additions & 1 deletion src/Http/Requests/RequestResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,62 @@
class RequestResolver
{

/**
* @var array
*/
private static array $custom = [];

/**
* @var string
*/
private string $type;

/**
* Register a custom binding for a query.
*
* @param string $resourceType
* @param string $class
*/
public static function registerQuery(string $resourceType, string $class): void
{
self::register('Query', $resourceType, $class);
}

/**
* Register a custom binding for a collection query.
*
* @param string $resourceType
* @param string $class
*/
public static function registerCollectionQuery(string $resourceType, string $class): void
{
self::register('CollectionQuery', $resourceType, $class);
}

/**
* Register a custom binding for a resource request.
*
* @param string $resourceType
* @param string $class
*/
public static function registerRequest(string $resourceType, string $class): void
{
self::register('Request', $resourceType, $class);
}

/**
* Register a custom binding.
*
* @param string $type
* @param string $resourceType
* @param string $class
*/
private static function register(string $type, string $resourceType, string $class): void
{
self::$custom[$type] = self::$custom[$type] ?? [];
self::$custom[$type][$resourceType] = $class;
}

/**
* ResourceRequest constructor.
*
Expand All @@ -55,7 +106,7 @@ public function __invoke(string $resourceType, bool $allowNull = false): ?FormRe
$app = app();

try {
$fqn = Str::replaceLast('Schema', $this->type, get_class(
$fqn = $this->custom($resourceType) ?: Str::replaceLast('Schema', $this->type, get_class(
$app->make(SchemaContainer::class)->schemaFor($resourceType)
));

Expand All @@ -78,4 +129,17 @@ public function __invoke(string $resourceType, bool $allowNull = false): ?FormRe
), 0, $ex);
}
}

/**
* Check whether a custom class has been registered for the resource type.
*
* @param string $resourceType
* @return string|null
*/
private function custom(string $resourceType): ?string
{
$values = self::$custom[$this->type] ?? [];

return $values[$resourceType] ?? null;
}
}
73 changes: 73 additions & 0 deletions tests/dummy/app/JsonApi/V1/Images/ImageSchema.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?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\JsonApi\V1\Images;

use App\Models\Image;
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\Pagination\PagePagination;
use LaravelJsonApi\Eloquent\Schema;

class ImageSchema extends Schema
{

/**
* The model the schema corresponds to.
*
* @var string
*/
public static string $model = Image::class;

/**
* @inheritDoc
*/
public function fields(): array
{
return [
ID::make()->uuid(),
DateTime::make('createdAt')->sortable()->readOnly(),
Str::make('url'),
DateTime::make('updatedAt')->sortable()->readOnly(),
];
}

/**
* @inheritDoc
*/
public function filters(): array
{
return [
WhereIn::make('id', $this->idColumn())->delimiter(','),
];
}

/**
* @inheritDoc
*/
public function pagination(): ?Paginator
{
return PagePagination::make()->withoutNestedMeta();
}

}
63 changes: 63 additions & 0 deletions tests/dummy/app/JsonApi/V1/Media/MediaCollectionQuery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?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\JsonApi\V1\Media;

use LaravelJsonApi\Laravel\Http\Requests\ResourceQuery;
use LaravelJsonApi\Validation\Rule as JsonApiRule;

class MediaCollectionQuery extends ResourceQuery
{

/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'fields' => [
'nullable',
'array',
JsonApiRule::fieldSets(),
],
'filter' => [
'nullable',
'array',
JsonApiRule::filter(['id']),
],
'include' => [
'nullable',
'string',
JsonApiRule::includePathsForPolymorph(),
],
'page' => [
'nullable',
'array',
JsonApiRule::notSupported(),
],
'sort' => [
'nullable',
'string',
JsonApiRule::notSupported(),
],
];
}
}
1 change: 1 addition & 0 deletions tests/dummy/app/JsonApi/V1/Posts/PostRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public function rules(): array
return [
'content' => ['required', 'string'],
'deletedAt' => ['nullable', JsonApiRule::dateTime()],
'media' => JsonApiRule::toMany(),
'slug' => ['required', 'string', $unique],
'synopsis' => ['required', 'string'],
'tags' => JsonApiRule::toMany(),
Expand Down
2 changes: 2 additions & 0 deletions tests/dummy/app/JsonApi/V1/Posts/PostResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ public function relationships($request): iterable
return [
$this->relation('author')->showDataIfLoaded(),
$this->relation('comments'),
$this->relation('media')
->withData(fn() => $this->schema->relationship('media')->value($this->resource)),
$this->relation('tags')->showDataIfLoaded(),
];
}
Expand Down
12 changes: 12 additions & 0 deletions tests/dummy/app/JsonApi/V1/Posts/PostSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use LaravelJsonApi\Eloquent\Fields\Relations\BelongsTo;
use LaravelJsonApi\Eloquent\Fields\Relations\BelongsToMany;
use LaravelJsonApi\Eloquent\Fields\Relations\HasMany;
use LaravelJsonApi\Eloquent\Fields\Relations\MorphToMany;
use LaravelJsonApi\Eloquent\Fields\SoftDelete;
use LaravelJsonApi\Eloquent\Fields\Str;
use LaravelJsonApi\Eloquent\Filters\OnlyTrashed;
Expand All @@ -48,6 +49,13 @@ class PostSchema extends Schema
*/
public static string $model = Post::class;

/**
* The maximum depth of include paths.
*
* @var int
*/
protected int $maxDepth = 3;

/**
* @inheritDoc
*/
Expand All @@ -60,6 +68,10 @@ public function fields(): array
Str::make('content'),
DateTime::make('createdAt')->sortable()->readOnly(),
SoftDelete::make('deletedAt')->sortable(),
MorphToMany::make('media', [
BelongsToMany::make('images'),
BelongsToMany::make('videos'),
]),
DateTime::make('publishedAt')->sortable(),
Str::make('slug'),
Str::make('synopsis'),
Expand Down
4 changes: 4 additions & 0 deletions tests/dummy/app/JsonApi/V1/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use App\Models\Video;
use Illuminate\Support\Facades\Auth;
use LaravelJsonApi\Core\Server\Server as BaseServer;
use LaravelJsonApi\Laravel\Http\Requests\RequestResolver;

class Server extends BaseServer
{
Expand All @@ -50,6 +51,8 @@ public function serving(): void
Video::creating(static function (Video $video) {
$video->owner()->associate(Auth::user());
});

RequestResolver::registerCollectionQuery('media', Media\MediaCollectionQuery::class);
}

/**
Expand All @@ -61,6 +64,7 @@ protected function allSchemas(): array
{
return [
Comments\CommentSchema::class,
Images\ImageSchema::class,
Posts\PostSchema::class,
Tags\TagSchema::class,
Users\UserSchema::class,
Expand Down
62 changes: 62 additions & 0 deletions tests/dummy/app/Models/Image.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?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;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;

class Image extends Model
{

use HasFactory;

/**
* @var bool
*/
public $incrementing = false;

/**
* @var string
*/
protected $primaryKey = 'uuid';

/**
* @var string
*/
protected $keyType = 'string';

/**
* @var string[]
*/
protected $fillable = ['url'];

/**
* @inheritDoc
*/
protected static function booting()
{
parent::booting();

self::creating(static function (self $model) {
$model->{$model->getKeyName()} = $model->{$model->getKeyName()} ?? Str::uuid()->toString();
});
}
}
Loading