Skip to content

Commit 2ec3972

Browse files
[JSON:API] Handles Nested Relationship (#58009)
* 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 * 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> * 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> * 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> * Update src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php --------- Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> Co-authored-by: StyleCI Bot <bot@styleci.io>
1 parent 7f75fb0 commit 2ec3972

File tree

2 files changed

+119
-55
lines changed

2 files changed

+119
-55
lines changed

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

Lines changed: 110 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Illuminate\Http\Resources\JsonApi\Concerns;
44

5+
use Generator;
56
use Illuminate\Contracts\Support\Arrayable;
67
use Illuminate\Database\Eloquent\Model;
78
use Illuminate\Database\Eloquent\Relations\AsPivot;
@@ -16,9 +17,9 @@
1617
use Illuminate\Http\Resources\MissingValue;
1718
use Illuminate\Support\Arr;
1819
use Illuminate\Support\Collection;
20+
use Illuminate\Support\LazyCollection;
1921
use Illuminate\Support\Str;
2022
use JsonSerializable;
21-
use WeakMap;
2223

2324
trait ResolvesJsonApiElements
2425
{
@@ -35,9 +36,9 @@ trait ResolvesJsonApiElements
3536
/**
3637
* Cached loaded relationships map.
3738
*
38-
* @var \WeakMap|null
39+
* @var array<int, array{0: \Illuminate\Http\Resources\JsonApi\JsonApiResource, 1: string, 2: string, 3: bool}|null
3940
*/
40-
protected $loadedRelationshipsMap;
41+
public $loadedRelationshipsMap;
4142

4243
/**
4344
* Cached loaded relationships identifers.
@@ -104,7 +105,7 @@ protected function resolveResourceObject(JsonApiRequest $request): array
104105
*
105106
* @throws ResourceIdentificationException
106107
*/
107-
protected function resolveResourceIdentifier(JsonApiRequest $request): string
108+
public function resolveResourceIdentifier(JsonApiRequest $request): string
108109
{
109110
if (! is_null($resourceId = $this->toId($request))) {
110111
return $resourceId;
@@ -123,7 +124,7 @@ protected function resolveResourceIdentifier(JsonApiRequest $request): string
123124
*
124125
* @throws ResourceIdentificationException
125126
*/
126-
protected function resolveResourceType(JsonApiRequest $request): string
127+
public function resolveResourceType(JsonApiRequest $request): string
127128
{
128129
if (! is_null($resourceType = $this->toType($request))) {
129130
return $resourceType;
@@ -195,7 +196,7 @@ protected function resolveResourceRelationshipIdentifiers(JsonApiRequest $reques
195196
*/
196197
protected function compileResourceRelationships(JsonApiRequest $request): void
197198
{
198-
if ($this->loadedRelationshipsMap instanceof WeakMap) {
199+
if (! is_null($this->loadedRelationshipsMap)) {
199200
return;
200201
}
201202

@@ -205,74 +206,124 @@ protected function compileResourceRelationships(JsonApiRequest $request): void
205206
};
206207

207208
$resourceRelationships = (new Collection($this->toRelationships($request)))
208-
->mapWithKeys(function ($value, $key) {
209-
$relationResolver = is_int($key) ? new RelationResolver($value) : new RelationResolver($key, $value);
210-
211-
return [$relationResolver->relationName => $relationResolver];
212-
})->filter(fn ($value, $key) => in_array($key, $sparseIncluded));
209+
->transform(fn ($value, $key) => is_int($key) ? new RelationResolver($value) : new RelationResolver($key, $value))
210+
->mapWithKeys(fn ($relationResolver) => [$relationResolver->relationName => $relationResolver])
211+
->filter(fn ($value, $key) => in_array($key, $sparseIncluded));
213212

214213
$resourceRelationshipKeys = $resourceRelationships->keys();
215214

216215
$this->resource->loadMissing($resourceRelationshipKeys->all() ?? []);
217216

218-
$this->loadedRelationshipsMap = new WeakMap;
217+
$this->loadedRelationshipsMap = [];
219218

220-
$this->loadedRelationshipIdentifiers = $resourceRelationships->mapWithKeys(function (RelationResolver $relationResolver, $key) use ($request) {
221-
$relatedModels = $relationResolver->handle($this->resource);
222-
$relatedResourceClass = $relationResolver->resourceClass();
219+
$this->loadedRelationshipIdentifiers = (new LazyCollection(function () use ($request, $resourceRelationships) {
220+
foreach ($resourceRelationships as $relationName => $relationResolver) {
221+
$relatedModels = $relationResolver->handle($this->resource);
222+
$relatedResourceClass = $relationResolver->resourceClass();
223223

224-
if (! is_null($relatedModels)) {
225-
$relatedModels->loadMissing($request->sparseIncluded($key));
226-
}
224+
if (! is_null($relatedModels)) {
225+
$relatedModels->loadMissing($request->sparseIncluded($relationName));
226+
}
227227

228-
// Relationship is a collection of models...
229-
if ($relatedModels instanceof Collection) {
230-
$relatedModels = $relatedModels->values();
228+
yield from $this->compileResourceRelationshipUsingResolver(
229+
$this->resource,
230+
$relationResolver,
231+
$relatedModels,
232+
$request
233+
);
234+
}
235+
}))->all();
236+
}
231237

232-
if ($relatedModels->isEmpty()) {
233-
return [$key => ['data' => $relatedModels]];
234-
}
238+
/**
239+
* Compile resource relations.
240+
*/
241+
protected function compileResourceRelationshipUsingResolver(
242+
mixed $resource,
243+
RelationResolver $relationResolver,
244+
Collection|Model|null $relatedModels,
245+
JsonApiRequest $request
246+
): Generator {
247+
$relationName = $relationResolver->relationName;
248+
$resourceClass = $relationResolver->resourceClass();
249+
250+
// Relationship is a collection of models...
251+
if ($relatedModels instanceof Collection) {
252+
$relatedModels = $relatedModels->values();
253+
254+
if ($relatedModels->isEmpty()) {
255+
yield $relationName => ['data' => $relatedModels];
256+
257+
return;
258+
}
235259

236-
$relationship = $this->resource->{$key}();
260+
$relationship = $resource->{$relationName}();
261+
$isUnique = ! $relationship instanceof BelongsToMany;
237262

238-
$isUnique = ! $relationship instanceof BelongsToMany;
263+
yield $relationName => ['data' => $relatedModels->map(function ($relatedModel) use ($request, $resourceClass, $isUnique) {
264+
$relatedResource = rescue(fn () => $relatedModel->toResource($resourceClass), new JsonApiResource($relatedModel));
239265

240-
$key = $relationResolver->resourceType($relatedModels, $request);
266+
return transform(
267+
[$relatedResource->resolveResourceType($request), $relatedResource->resolveResourceIdentifier($request)],
268+
function ($uniqueKey) use ($request, $relatedModel, $relatedResource, $isUnique) {
269+
$this->loadedRelationshipsMap[] = [$relatedResource, ...$uniqueKey, $isUnique];
241270

242-
return [$key => ['data' => $relatedModels->map(function ($relation) use ($key, $relatedResourceClass, $isUnique) {
243-
return transform([$key, static::resourceIdFromModel($relation)], function ($uniqueKey) use ($relation, $relatedResourceClass, $isUnique) {
244-
$this->loadedRelationshipsMap[$relation] = [...$uniqueKey, $relatedResourceClass, $isUnique];
271+
$this->compileIncludedNestedRelationshipsMap($relatedModel, $relatedResource, $request);
245272

246273
return [
247274
'id' => $uniqueKey[1],
248275
'type' => $uniqueKey[0],
249276
];
250-
});
251-
})]];
252-
}
277+
}
278+
);
279+
})->all()];
253280

254-
// Relationship is a single model...
255-
$relatedModel = $relatedModels;
281+
return;
282+
}
256283

257-
if (is_null($relatedModel)) {
258-
return [$key => null];
259-
} elseif ($relatedModel instanceof Pivot ||
260-
in_array(AsPivot::class, class_uses_recursive($relatedModel), true)) {
261-
return [$key => new MissingValue];
262-
}
284+
// Relationship is a single model...
285+
$relatedModel = $relatedModels;
263286

264-
return [$key => ['data' => transform(
265-
[$relationResolver->resourceType($relatedModel, $request), static::resourceIdFromModel($relatedModel)],
266-
function ($uniqueKey) use ($relatedModel, $relatedResourceClass) {
267-
$this->loadedRelationshipsMap[$relatedModel] = [...$uniqueKey, $relatedResourceClass, true];
287+
if (is_null($relatedModel)) {
288+
yield $relationName => null;
268289

269-
return [
270-
'id' => $uniqueKey[1],
271-
'type' => $uniqueKey[0],
272-
];
273-
}
274-
)]];
275-
})->all();
290+
return;
291+
} elseif ($relatedModel instanceof Pivot ||
292+
in_array(AsPivot::class, class_uses_recursive($relatedModel), true)) {
293+
yield $relationName => new MissingValue;
294+
295+
return;
296+
}
297+
298+
$relatedResource = rescue(fn () => $relatedModel->toResource($resourceClass), new JsonApiResource($relatedModel));
299+
300+
yield $relationName => ['data' => transform(
301+
[$relatedResource->resolveResourceType($request), $relatedResource->resolveResourceIdentifier($request)],
302+
function ($uniqueKey) use ($relatedModel, $relatedResource, $request) {
303+
$this->loadedRelationshipsMap[] = [$relatedResource, ...$uniqueKey, true];
304+
305+
$this->compileIncludedNestedRelationshipsMap($relatedModel, $relatedResource, $request);
306+
307+
return [
308+
'id' => $uniqueKey[1],
309+
'type' => $uniqueKey[0],
310+
];
311+
}
312+
)];
313+
}
314+
315+
/**
316+
* Compile included relationships map.
317+
*/
318+
protected function compileIncludedNestedRelationshipsMap(Model $relation, JsonApiResource $resource, JsonApiRequest $request): void
319+
{
320+
(new Collection($resource->toRelationships($request)))
321+
->transform(fn ($value, $key) => is_int($key) ? new RelationResolver($value) : new RelationResolver($key, $value))
322+
->mapWithKeys(fn ($relationResolver) => [$relationResolver->relationName => $relationResolver])
323+
->filter(fn ($value, $key) => in_array($key, array_keys($relation->getRelations())))
324+
->each(function ($relationResolver, $key) use ($relation, $request) {
325+
$this->compileResourceRelationshipUsingResolver($relation, $relationResolver, $relation->getRelation($key), $request);
326+
});
276327
}
277328

278329
/**
@@ -288,10 +339,10 @@ public function resolveIncludedResources(JsonApiRequest $request): array
288339

289340
$relations = new Collection;
290341

291-
foreach ($this->loadedRelationshipsMap as $relation => $value) {
292-
[$type, $id, $relatedResourceClass, $isUnique] = $value;
342+
$index = 0;
293343

294-
$resourceInstance = rescue(fn () => $relation->toResource($relatedResourceClass), new JsonApiResource($relation), false);
344+
while ($index < count($this->loadedRelationshipsMap)) {
345+
[$resourceInstance, $type, $id, $isUnique] = $this->loadedRelationshipsMap[$index];
295346

296347
if (! $resourceInstance instanceof JsonApiResource &&
297348
$resourceInstance instanceof JsonResource) {
@@ -300,6 +351,8 @@ public function resolveIncludedResources(JsonApiRequest $request): array
300351

301352
$relationsData = $resourceInstance->withoutRequestQueryString()->withIncludedFromLoadedRelationships()->resolve($request);
302353

354+
array_push($this->loadedRelationshipsMap, ...$resourceInstance->loadedRelationshipsMap);
355+
303356
$relations->push(array_filter([
304357
'id' => $id,
305358
'type' => $type,
@@ -309,6 +362,8 @@ public function resolveIncludedResources(JsonApiRequest $request): array
309362
'links' => Arr::get($relationsData, 'data.links'),
310363
'meta' => Arr::get($relationsData, 'data.meta'),
311364
]));
365+
366+
$index++;
312367
}
313368

314369
return $relations->uniqueStrict(fn ($relation) => $relation['_uniqueKey'])

tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ public function test_it_can_resolve_relationship_with_nested_relationship()
267267

268268
$this->getJson("/posts/{$post1->getKey()}?".http_build_query(['include' => 'author,comments.commenter']))
269269
->assertHeader('Content-type', 'application/vnd.api+json')
270+
->dump()
270271
->assertExactJson([
271272
'data' => [
272273
'attributes' => [
@@ -313,6 +314,14 @@ public function test_it_can_resolve_relationship_with_nested_relationship()
313314
],
314315
],
315316
],
317+
[
318+
'attributes' => [
319+
'email' => $user->email,
320+
'name' => $user->name,
321+
],
322+
'id' => (string) $user->getKey(),
323+
'type' => 'users',
324+
],
316325
],
317326
])
318327
->assertJsonMissing(['jsonapi']);

0 commit comments

Comments
 (0)