Skip to content

Commit be536df

Browse files
crynoboneStyleCIBottaylorotwell
authored
[JSON:API] Supports JsonApiResource::toRelationships() (#57646)
* [JSON:API] Supports `JsonApiResource::toRelationships()` Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * Update JsonApiResource.php * only include relationship if defined * formatting * tweak how relations work * work on relationships * fixes implementation Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * Add `JsonApiRequest` Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * formatting * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * Apply fixes from StyleCI * Add `make:resource` with `--json-api` option Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * Apply suggestions from code review * ensure request is always `JsonApiRequest` * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * Apply fixes from StyleCI --------- Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> Co-authored-by: StyleCI Bot <bot@styleci.io> Co-authored-by: Taylor Otwell <taylor@laravel.com>
1 parent 4451594 commit be536df

24 files changed

+930
-178
lines changed

src/Illuminate/Foundation/Console/ResourceMakeCommand.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,11 @@ public function handle()
5151
*/
5252
protected function getStub()
5353
{
54-
return $this->collection()
55-
? $this->resolveStubPath('/stubs/resource-collection.stub')
56-
: $this->resolveStubPath('/stubs/resource.stub');
54+
return match (true) {
55+
$this->collection() => $this->resolveStubPath('/stubs/resource-collection.stub'),
56+
$this->option('json-api') => $this->resolveStubPath('/stubs/resource-json-api.stub'),
57+
default => $this->resolveStubPath('/stubs/resource.stub'),
58+
};
5759
}
5860

5961
/**
@@ -101,6 +103,7 @@ protected function getOptions()
101103
return [
102104
['force', 'f', InputOption::VALUE_NONE, 'Create the class even if the resource already exists'],
103105
['collection', 'c', InputOption::VALUE_NONE, 'Create a resource collection'],
106+
['json-api', 'j', InputOption::VALUE_NONE, 'Create a JSON:API resource'],
104107
];
105108
}
106109
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace {{ namespace }};
4+
5+
use Illuminate\Http\Request;
6+
use Illuminate\Http\Resources\JsonApi\JsonApiResource;
7+
8+
class {{ class }} extends JsonApiResource
9+
{
10+
/**
11+
* Transform the resource into an array.
12+
*
13+
* @return list<string>|array<string, mixed>
14+
*/
15+
public function toAttributes(Request $request): array
16+
{
17+
return parent::toAttributes($request);
18+
}
19+
}

src/Illuminate/Http/Resources/Json/JsonResource.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ protected static function newCollection($resource)
112112
public function resolve($request = null)
113113
{
114114
$data = $this->toAttributes(
115-
$request ?: Container::getInstance()->make('request')
115+
$request ?: $this->resolveRequestFromContainer()
116116
);
117117

118118
if ($data instanceof Arrayable) {
@@ -230,6 +230,16 @@ public function withResponse(Request $request, JsonResponse $response)
230230
//
231231
}
232232

233+
/**
234+
* Resolve the HTTP request instance from container.
235+
*
236+
* @return \Illuminate\Http\Request
237+
*/
238+
protected function resolveRequestFromContainer()
239+
{
240+
return Container::getInstance()->make('request');
241+
}
242+
233243
/**
234244
* Set the string that should wrap the outer-most resource array.
235245
*
@@ -260,7 +270,7 @@ public static function withoutWrapping()
260270
public function response($request = null)
261271
{
262272
return $this->toResponse(
263-
$request ?: Container::getInstance()->make('request')
273+
$request ?: $this->resolveRequestFromContainer()
264274
);
265275
}
266276

@@ -282,7 +292,7 @@ public function toResponse($request)
282292
*/
283293
public function jsonSerialize(): array
284294
{
285-
return $this->resolve(Container::getInstance()->make('request'));
295+
return $this->resolve($this->resolveRequestFromContainer());
286296
}
287297

288298
/**

src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
namespace Illuminate\Http\Resources\JsonApi;
44

5+
use Illuminate\Container\Container;
56
use Illuminate\Http\JsonResponse;
67
use Illuminate\Http\Request;
78

89
class AnonymousResourceCollection extends \Illuminate\Http\Resources\Json\AnonymousResourceCollection
910
{
11+
use Concerns\ResolvesJsonApiRequest;
12+
1013
/**
1114
* Get any additional data that should be returned with the resource array.
1215
*
@@ -20,7 +23,6 @@ public function with($request)
2023
'included' => $this->collection
2124
->map(fn ($resource) => $resource->resolveIncludedResources($request))
2225
->flatten(depth: 1)
23-
->uniqueStrict(fn ($relation): array => [$relation['id'], $relation['type']])
2426
->all(),
2527
...($implementation = JsonApiResource::$jsonApiInformation)
2628
? ['jsonapi' => $implementation]
@@ -54,4 +56,27 @@ public function withResponse(Request $request, JsonResponse $response): void
5456
{
5557
$response->header('Content-Type', 'application/vnd.api+json');
5658
}
59+
60+
/**
61+
* Create an HTTP response that represents the object.
62+
*
63+
* @param \Illuminate\Http\Request $request
64+
* @return \Illuminate\Http\JsonResponse
65+
*/
66+
#[\Override]
67+
public function toResponse($request)
68+
{
69+
return parent::toResponse($this->resolveJsonApiRequestFrom($request));
70+
}
71+
72+
/**
73+
* Resolve the HTTP request instance from container.
74+
*
75+
* @return \Illuminate\Http\Resources\JsonApi\SparseRequest
76+
*/
77+
#[\Override]
78+
protected function resolveRequestFromContainer()
79+
{
80+
return $this->resolveJsonApiRequestFrom(Container::getInstance()->make('request'));
81+
}
5782
}

src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php

Lines changed: 87 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@
44

55
use Illuminate\Contracts\Support\Arrayable;
66
use Illuminate\Database\Eloquent\Model;
7+
use Illuminate\Database\Eloquent\Relations\AsPivot;
8+
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
9+
use Illuminate\Database\Eloquent\Relations\Pivot;
710
use Illuminate\Database\Eloquent\Relations\Relation;
8-
use Illuminate\Http\Request;
911
use Illuminate\Http\Resources\Json\JsonResource;
1012
use Illuminate\Http\Resources\JsonApi\Exceptions\ResourceIdentificationException;
13+
use Illuminate\Http\Resources\JsonApi\JsonApiRequest;
1114
use Illuminate\Http\Resources\JsonApi\JsonApiResource;
15+
use Illuminate\Http\Resources\MissingValue;
1216
use Illuminate\Support\Arr;
1317
use Illuminate\Support\Collection;
1418
use Illuminate\Support\Str;
@@ -32,13 +36,15 @@ trait ResolvesJsonApiElements
3236
/**
3337
* Resolves `data` for the resource.
3438
*/
35-
public function resolveResourceData(Request $request): array
39+
public function resolveResourceData(JsonApiRequest $request): array
3640
{
41+
$resourceType = $this->resolveResourceType($request);
42+
3743
return [
3844
'id' => $this->resolveResourceIdentifier($request),
39-
'type' => $this->resolveResourceType($request),
45+
'type' => $resourceType,
4046
...(new Collection([
41-
'attributes' => $this->resolveResourceAttributes($request),
47+
'attributes' => $this->resolveResourceAttributes($request, $resourceType),
4248
'relationships' => $this->resolveResourceRelationshipIdentifiers($request),
4349
'links' => $this->resolveResourceLinks($request),
4450
'meta' => $this->resolveResourceMetaInformation($request),
@@ -53,7 +59,7 @@ public function resolveResourceData(Request $request): array
5359
*
5460
* @throws ResourceIdentificationException
5561
*/
56-
protected function resolveResourceIdentifier(Request $request): string
62+
protected function resolveResourceIdentifier(JsonApiRequest $request): string
5763
{
5864
if (! is_null($resourceId = $this->toId($request))) {
5965
return $resourceId;
@@ -72,7 +78,7 @@ protected function resolveResourceIdentifier(Request $request): string
7278
*
7379
* @throws ResourceIdentificationException
7480
*/
75-
protected function resolveResourceType(Request $request): string
81+
protected function resolveResourceType(JsonApiRequest $request): string
7682
{
7783
if (! is_null($resourceType = $this->toType($request))) {
7884
return $resourceType;
@@ -91,7 +97,7 @@ protected function resolveResourceType(Request $request): string
9197
*
9298
* @throws \RuntimeException
9399
*/
94-
protected function resolveResourceAttributes(Request $request): array
100+
protected function resolveResourceAttributes(JsonApiRequest $request, string $resourceType): array
95101
{
96102
$data = $this->toAttributes($request);
97103

@@ -101,8 +107,11 @@ protected function resolveResourceAttributes(Request $request): array
101107
$data = $data->jsonSerialize();
102108
}
103109

110+
$sparseFieldset = $request->sparseFields($resourceType);
111+
104112
$data = (new Collection($data))
105113
->mapWithKeys(fn ($value, $key) => is_int($key) ? [$value => $this->resource->{$value}] : [$key => $value])
114+
->when(! empty($sparseFieldset), fn ($attributes) => $attributes->only($sparseFieldset))
106115
->transform(fn ($value) => value($value, $request))
107116
->all();
108117

@@ -116,7 +125,7 @@ protected function resolveResourceAttributes(Request $request): array
116125
*
117126
* @throws \RuntimeException
118127
*/
119-
protected function resolveResourceRelationshipIdentifiers(Request $request): array
128+
protected function resolveResourceRelationshipIdentifiers(JsonApiRequest $request): array
120129
{
121130
if (! $this->resource instanceof Model) {
122131
return [];
@@ -125,85 +134,123 @@ protected function resolveResourceRelationshipIdentifiers(Request $request): arr
125134
$this->compileResourceRelationships($request);
126135

127136
return [
128-
...$this->loadedRelationshipIdentifiers,
137+
...(new Collection($this->filter($this->loadedRelationshipIdentifiers)))
138+
->map(function ($relation) {
139+
return ! is_null($relation) ? $relation : ['data' => []];
140+
})->all(),
129141
];
130142
}
131143

132144
/**
133145
* Compile resource relationships.
134146
*/
135-
protected function compileResourceRelationships(Request $request): void
147+
protected function compileResourceRelationships(JsonApiRequest $request): void
136148
{
137149
if ($this->loadedRelationshipsMap instanceof WeakMap) {
138150
return;
139151
}
140152

141-
$this->loadedRelationshipsMap = new WeakMap;
153+
$sparseIncluded = $request->sparseIncluded();
154+
155+
$resourceRelationships = (new Collection($this->toRelationships($request)))
156+
->mapWithKeys(fn ($value, $key) => is_int($key) ? [$value => fn () => $this->resource->{$value}] : [$key => $value])
157+
->filter(fn ($value, $key) => in_array($key, $sparseIncluded));
158+
159+
$resourceRelationshipKeys = $resourceRelationships->keys();
160+
161+
$this->resource->loadMissing($resourceRelationshipKeys->all());
142162

143-
$this->loadedRelationshipIdentifiers = (new Collection($this->resource->getRelations()))
144-
->mapWithKeys(function ($relations, $key) {
145-
if ($relations instanceof Collection) {
146-
if ($relations->isEmpty()) {
147-
return [$key => ['data' => $relations]];
148-
}
163+
$this->loadedRelationshipsMap = new WeakMap;
149164

150-
$key = static::resourceTypeFromModel($relations->first());
165+
$this->loadedRelationshipIdentifiers = $resourceRelationships->mapWithKeys(function ($relationResolver, $key) {
166+
$relatedModels = value($relationResolver);
151167

152-
return [$key => ['data' => $relations->map(function ($relation) use ($key) {
153-
return transform([$key, static::resourceIdFromModel($relation)], function ($uniqueKey) use ($relation) {
154-
$this->loadedRelationshipsMap[$relation] = $uniqueKey;
168+
// Relationship is a collection of models...
169+
if ($relatedModels instanceof Collection) {
170+
$relatedModels = $relatedModels->values();
155171

156-
return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]];
157-
});
158-
})]];
172+
if ($relatedModels->isEmpty()) {
173+
return [$key => ['data' => $relatedModels]];
159174
}
160175

161-
return [$key => ['data' => transform(
162-
[static::resourceTypeFromModel($relations), static::resourceIdFromModel($relations)],
163-
function ($uniqueKey) use ($relations) {
164-
$this->loadedRelationshipsMap[$relations] = $uniqueKey;
176+
$relationship = $this->resource->{$key}();
177+
178+
$isUnique = ! $relationship instanceof BelongsToMany;
179+
180+
$key = static::resourceTypeFromModel($relatedModels->first());
181+
182+
return [$key => ['data' => $relatedModels->map(function ($relation) use ($key, $isUnique) {
183+
return transform([$key, static::resourceIdFromModel($relation)], function ($uniqueKey) use ($relation, $isUnique) {
184+
$this->loadedRelationshipsMap[$relation] = [...$uniqueKey, $isUnique];
165185

166186
return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]];
167-
}
168-
)]];
169-
})->all();
187+
});
188+
})]];
189+
}
190+
191+
// Relationship is a single model...
192+
$relatedModel = $relatedModels;
193+
194+
if (is_null($relatedModel)) {
195+
return [$key => null];
196+
} elseif ($relatedModel instanceof Pivot ||
197+
in_array(AsPivot::class, class_uses_recursive($relatedModel), true)) {
198+
return [$key => new MissingValue];
199+
}
200+
201+
return [$key => ['data' => [transform(
202+
[static::resourceTypeFromModel($relatedModel), static::resourceIdFromModel($relatedModel)],
203+
function ($uniqueKey) use ($relatedModel) {
204+
$this->loadedRelationshipsMap[$relatedModel] = [...$uniqueKey, true];
205+
206+
return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]];
207+
}
208+
)]]];
209+
})->all();
170210
}
171211

172212
/**
173213
* Resolves `included` for the resource.
174214
*/
175-
public function resolveIncludedResources(Request $request): array
215+
public function resolveIncludedResources(JsonApiRequest $request): array
176216
{
217+
if (! $this->resource instanceof Model) {
218+
return [];
219+
}
220+
177221
$this->compileResourceRelationships($request);
178222

179223
$relations = new Collection;
180224

181-
foreach ($this->loadedRelationshipsMap as $relation => $uniqueKey) {
225+
foreach ($this->loadedRelationshipsMap as $relation => $value) {
182226
$resourceInstance = rescue(fn () => $relation->toResource(), new JsonApiResource($relation), false);
183227

184228
if (! $resourceInstance instanceof JsonApiResource &&
185229
$resourceInstance instanceof JsonResource) {
186230
$resourceInstance = new JsonApiResource($resourceInstance->resource);
187231
}
188232

233+
[$type, $id, $isUnique] = $value;
234+
189235
$relations->push([
190-
'id' => $uniqueKey[1],
191-
'type' => $uniqueKey[0],
236+
'id' => $id,
237+
'type' => $type,
238+
'_uniqueKey' => $isUnique === true ? [$id, $type] : [$id, $type, (string) Str::random()],
192239
'attributes' => Arr::get($resourceInstance->resolve($request), 'data.attributes', []),
193240
]);
194241
}
195242

196-
return $relations->uniqueStrict(
197-
fn ($relation): array => [$relation['id'], $relation['type']]
198-
)->all();
243+
return $relations->uniqueStrict(fn ($relation) => $relation['_uniqueKey'])
244+
->map(fn ($relation) => Arr::except($relation, ['_uniqueKey']))
245+
->all();
199246
}
200247

201248
/**
202249
* Resolve the links for the resource.
203250
*
204251
* @return array<string, mixed>
205252
*/
206-
protected function resolveResourceLinks(Request $request): array
253+
protected function resolveResourceLinks(JsonApiRequest $request): array
207254
{
208255
return $this->toLinks($request);
209256
}
@@ -213,7 +260,7 @@ protected function resolveResourceLinks(Request $request): array
213260
*
214261
* @return array<string, mixed>
215262
*/
216-
protected function resolveResourceMetaInformation(Request $request): array
263+
protected function resolveResourceMetaInformation(JsonApiRequest $request): array
217264
{
218265
return $this->toMeta($request);
219266
}

0 commit comments

Comments
 (0)