Skip to content

Commit eaccb9c

Browse files
[Feature] Support polymorphic to-many relations (#47)
Refer to documentation for usage.
1 parent b73cc68 commit eaccb9c

21 files changed

+873
-45
lines changed

CHANGELOG.md

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

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

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"laravel-json-api/encoder-neomerx": "^1.0.0-alpha.4",
3131
"laravel-json-api/exceptions": "^1.0.0-alpha.2",
3232
"laravel-json-api/spec": "^1.0.0-alpha.4",
33-
"laravel-json-api/validation": "^1.0.0-alpha.4",
33+
"laravel-json-api/validation": "^1.0.0-alpha.5",
3434
"laravel/framework": "^8.0"
3535
},
3636
"require-dev": {

src/Http/Requests/RequestResolver.php

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,62 @@
3030
class RequestResolver
3131
{
3232

33+
/**
34+
* @var array
35+
*/
36+
private static array $custom = [];
37+
3338
/**
3439
* @var string
3540
*/
3641
private string $type;
3742

43+
/**
44+
* Register a custom binding for a query.
45+
*
46+
* @param string $resourceType
47+
* @param string $class
48+
*/
49+
public static function registerQuery(string $resourceType, string $class): void
50+
{
51+
self::register('Query', $resourceType, $class);
52+
}
53+
54+
/**
55+
* Register a custom binding for a collection query.
56+
*
57+
* @param string $resourceType
58+
* @param string $class
59+
*/
60+
public static function registerCollectionQuery(string $resourceType, string $class): void
61+
{
62+
self::register('CollectionQuery', $resourceType, $class);
63+
}
64+
65+
/**
66+
* Register a custom binding for a resource request.
67+
*
68+
* @param string $resourceType
69+
* @param string $class
70+
*/
71+
public static function registerRequest(string $resourceType, string $class): void
72+
{
73+
self::register('Request', $resourceType, $class);
74+
}
75+
76+
/**
77+
* Register a custom binding.
78+
*
79+
* @param string $type
80+
* @param string $resourceType
81+
* @param string $class
82+
*/
83+
private static function register(string $type, string $resourceType, string $class): void
84+
{
85+
self::$custom[$type] = self::$custom[$type] ?? [];
86+
self::$custom[$type][$resourceType] = $class;
87+
}
88+
3889
/**
3990
* ResourceRequest constructor.
4091
*
@@ -55,7 +106,7 @@ public function __invoke(string $resourceType, bool $allowNull = false): ?FormRe
55106
$app = app();
56107

57108
try {
58-
$fqn = Str::replaceLast('Schema', $this->type, get_class(
109+
$fqn = $this->custom($resourceType) ?: Str::replaceLast('Schema', $this->type, get_class(
59110
$app->make(SchemaContainer::class)->schemaFor($resourceType)
60111
));
61112

@@ -78,4 +129,17 @@ public function __invoke(string $resourceType, bool $allowNull = false): ?FormRe
78129
), 0, $ex);
79130
}
80131
}
132+
133+
/**
134+
* Check whether a custom class has been registered for the resource type.
135+
*
136+
* @param string $resourceType
137+
* @return string|null
138+
*/
139+
private function custom(string $resourceType): ?string
140+
{
141+
$values = self::$custom[$this->type] ?? [];
142+
143+
return $values[$resourceType] ?? null;
144+
}
81145
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
/*
3+
* Copyright 2021 Cloud Creativity Limited
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace App\JsonApi\V1\Images;
21+
22+
use App\Models\Image;
23+
use LaravelJsonApi\Eloquent\Contracts\Paginator;
24+
use LaravelJsonApi\Eloquent\Fields\DateTime;
25+
use LaravelJsonApi\Eloquent\Fields\ID;
26+
use LaravelJsonApi\Eloquent\Fields\Relations\BelongsToMany;
27+
use LaravelJsonApi\Eloquent\Fields\Str;
28+
use LaravelJsonApi\Eloquent\Filters\WhereIn;
29+
use LaravelJsonApi\Eloquent\Pagination\PagePagination;
30+
use LaravelJsonApi\Eloquent\Schema;
31+
32+
class ImageSchema extends Schema
33+
{
34+
35+
/**
36+
* The model the schema corresponds to.
37+
*
38+
* @var string
39+
*/
40+
public static string $model = Image::class;
41+
42+
/**
43+
* @inheritDoc
44+
*/
45+
public function fields(): array
46+
{
47+
return [
48+
ID::make()->uuid(),
49+
DateTime::make('createdAt')->sortable()->readOnly(),
50+
Str::make('url'),
51+
DateTime::make('updatedAt')->sortable()->readOnly(),
52+
];
53+
}
54+
55+
/**
56+
* @inheritDoc
57+
*/
58+
public function filters(): array
59+
{
60+
return [
61+
WhereIn::make('id', $this->idColumn())->delimiter(','),
62+
];
63+
}
64+
65+
/**
66+
* @inheritDoc
67+
*/
68+
public function pagination(): ?Paginator
69+
{
70+
return PagePagination::make()->withoutNestedMeta();
71+
}
72+
73+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
/*
3+
* Copyright 2021 Cloud Creativity Limited
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace App\JsonApi\V1\Media;
21+
22+
use LaravelJsonApi\Laravel\Http\Requests\ResourceQuery;
23+
use LaravelJsonApi\Validation\Rule as JsonApiRule;
24+
25+
class MediaCollectionQuery extends ResourceQuery
26+
{
27+
28+
/**
29+
* Get the validation rules that apply to the request.
30+
*
31+
* @return array
32+
*/
33+
public function rules(): array
34+
{
35+
return [
36+
'fields' => [
37+
'nullable',
38+
'array',
39+
JsonApiRule::fieldSets(),
40+
],
41+
'filter' => [
42+
'nullable',
43+
'array',
44+
JsonApiRule::filter(['id']),
45+
],
46+
'include' => [
47+
'nullable',
48+
'string',
49+
JsonApiRule::includePathsForPolymorph(),
50+
],
51+
'page' => [
52+
'nullable',
53+
'array',
54+
JsonApiRule::notSupported(),
55+
],
56+
'sort' => [
57+
'nullable',
58+
'string',
59+
JsonApiRule::notSupported(),
60+
],
61+
];
62+
}
63+
}

tests/dummy/app/JsonApi/V1/Posts/PostRequest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public function rules(): array
4141
return [
4242
'content' => ['required', 'string'],
4343
'deletedAt' => ['nullable', JsonApiRule::dateTime()],
44+
'media' => JsonApiRule::toMany(),
4445
'slug' => ['required', 'string', $unique],
4546
'synopsis' => ['required', 'string'],
4647
'tags' => JsonApiRule::toMany(),

tests/dummy/app/JsonApi/V1/Posts/PostResource.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ public function relationships($request): iterable
5656
return [
5757
$this->relation('author')->showDataIfLoaded(),
5858
$this->relation('comments'),
59+
$this->relation('media')
60+
->withData(fn() => $this->schema->relationship('media')->value($this->resource)),
5961
$this->relation('tags')->showDataIfLoaded(),
6062
];
6163
}

tests/dummy/app/JsonApi/V1/Posts/PostSchema.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use LaravelJsonApi\Eloquent\Fields\Relations\BelongsTo;
2727
use LaravelJsonApi\Eloquent\Fields\Relations\BelongsToMany;
2828
use LaravelJsonApi\Eloquent\Fields\Relations\HasMany;
29+
use LaravelJsonApi\Eloquent\Fields\Relations\MorphToMany;
2930
use LaravelJsonApi\Eloquent\Fields\SoftDelete;
3031
use LaravelJsonApi\Eloquent\Fields\Str;
3132
use LaravelJsonApi\Eloquent\Filters\OnlyTrashed;
@@ -48,6 +49,13 @@ class PostSchema extends Schema
4849
*/
4950
public static string $model = Post::class;
5051

52+
/**
53+
* The maximum depth of include paths.
54+
*
55+
* @var int
56+
*/
57+
protected int $maxDepth = 3;
58+
5159
/**
5260
* @inheritDoc
5361
*/
@@ -60,6 +68,10 @@ public function fields(): array
6068
Str::make('content'),
6169
DateTime::make('createdAt')->sortable()->readOnly(),
6270
SoftDelete::make('deletedAt')->sortable(),
71+
MorphToMany::make('media', [
72+
BelongsToMany::make('images'),
73+
BelongsToMany::make('videos'),
74+
]),
6375
DateTime::make('publishedAt')->sortable(),
6476
Str::make('slug'),
6577
Str::make('synopsis'),

tests/dummy/app/JsonApi/V1/Server.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use App\Models\Video;
2525
use Illuminate\Support\Facades\Auth;
2626
use LaravelJsonApi\Core\Server\Server as BaseServer;
27+
use LaravelJsonApi\Laravel\Http\Requests\RequestResolver;
2728

2829
class Server extends BaseServer
2930
{
@@ -50,6 +51,8 @@ public function serving(): void
5051
Video::creating(static function (Video $video) {
5152
$video->owner()->associate(Auth::user());
5253
});
54+
55+
RequestResolver::registerCollectionQuery('media', Media\MediaCollectionQuery::class);
5356
}
5457

5558
/**
@@ -61,6 +64,7 @@ protected function allSchemas(): array
6164
{
6265
return [
6366
Comments\CommentSchema::class,
67+
Images\ImageSchema::class,
6468
Posts\PostSchema::class,
6569
Tags\TagSchema::class,
6670
Users\UserSchema::class,

tests/dummy/app/Models/Image.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
/*
3+
* Copyright 2021 Cloud Creativity Limited
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace App\Models;
21+
22+
use Illuminate\Database\Eloquent\Factories\HasFactory;
23+
use Illuminate\Database\Eloquent\Model;
24+
use Illuminate\Support\Str;
25+
26+
class Image extends Model
27+
{
28+
29+
use HasFactory;
30+
31+
/**
32+
* @var bool
33+
*/
34+
public $incrementing = false;
35+
36+
/**
37+
* @var string
38+
*/
39+
protected $primaryKey = 'uuid';
40+
41+
/**
42+
* @var string
43+
*/
44+
protected $keyType = 'string';
45+
46+
/**
47+
* @var string[]
48+
*/
49+
protected $fillable = ['url'];
50+
51+
/**
52+
* @inheritDoc
53+
*/
54+
protected static function booting()
55+
{
56+
parent::booting();
57+
58+
self::creating(static function (self $model) {
59+
$model->{$model->getKeyName()} = $model->{$model->getKeyName()} ?? Str::uuid()->toString();
60+
});
61+
}
62+
}

0 commit comments

Comments
 (0)