From 51fad48676ecd8adfe3c4a8225793b25409394f0 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Fri, 28 Nov 2025 10:39:25 +0800 Subject: [PATCH 01/48] Filter empty `attributes` object Signed-off-by: Mior Muhammad Zaki --- .../Resources/JsonApi/Concerns/ResolvesJsonApiElements.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 2b2a4ef4e5da..683f013c1c9e 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -112,6 +112,7 @@ protected function resolveResourceAttributes(JsonApiRequest $request, string $re $data = (new Collection($data)) ->mapWithKeys(fn ($value, $key) => is_int($key) ? [$value => $this->resource->{$value}] : [$key => $value]) ->when(! empty($sparseFieldset), fn ($attributes) => $attributes->only($sparseFieldset)) + ->reject(fn ($value, $key) => $key === $this->resource->getKey()) ->transform(fn ($value) => value($value, $request)) ->all(); @@ -232,12 +233,12 @@ public function resolveIncludedResources(JsonApiRequest $request): array [$type, $id, $isUnique] = $value; - $relations->push([ + $relations->push(array_filter([ 'id' => $id, 'type' => $type, '_uniqueKey' => $isUnique === true ? [$id, $type] : [$id, $type, (string) Str::random()], - 'attributes' => Arr::get($resourceInstance->resolve($request), 'data.attributes', []), - ]); + 'attributes' => Arr::get($resourceInstance->resolve($request), 'data.attributes'), + ])); } return $relations->uniqueStrict(fn ($relation) => $relation['_uniqueKey']) From c62856b13267ba496bfe74016662bd3a057b80a6 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Fri, 28 Nov 2025 17:49:02 +0800 Subject: [PATCH 02/48] wip Signed-off-by: Mior Muhammad Zaki --- .../Resources/JsonApi/Concerns/ResolvesJsonApiElements.php | 4 ++-- .../Http/Resources/JsonApi/JsonApiCollectionTest.php | 3 ++- .../Http/Resources/JsonApi/JsonApiResourceTest.php | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 683f013c1c9e..b49c298a29ea 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -199,14 +199,14 @@ protected function compileResourceRelationships(JsonApiRequest $request): void return [$key => new MissingValue]; } - return [$key => ['data' => [transform( + return [$key => ['data' => transform( [static::resourceTypeFromModel($relatedModel), static::resourceIdFromModel($relatedModel)], function ($uniqueKey) use ($relatedModel) { $this->loadedRelationshipsMap[$relatedModel] = [...$uniqueKey, true]; return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]]; } - )]]]; + )]]; })->all(); } diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiCollectionTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiCollectionTest.php index b295eddd36cb..f45ef5999d3d 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiCollectionTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiCollectionTest.php @@ -122,7 +122,8 @@ public function testItCanGenerateJsonApiResponseWithRelationshipsUsingSparseIncl 'relationships' => [ 'profile' => [ 'data' => [ - ['id' => (string) $profile->getKey(), 'type' => 'profiles'], + 'id' => (string) $profile->getKey(), + 'type' => 'profiles', ], ], 'posts' => [ diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index fe166c5c10ba..1ba46b92810a 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -112,7 +112,8 @@ public function testItCanGenerateJsonApiResponseWithRelationshipsUsingSparseIncl ], 'profile' => [ 'data' => [ - ['id' => (string) $profile->getKey(), 'type' => 'profiles'], + 'id' => (string) $profile->getKey(), + 'type' => 'profiles', ], ], 'teams' => [ From 9964a2151439dd71bffed40063db2ab554f5a84b Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Fri, 28 Nov 2025 18:08:43 +0800 Subject: [PATCH 03/48] wip Signed-off-by: Mior Muhammad Zaki --- .../Resources/JsonApi/Concerns/ResolvesJsonApiElements.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index b49c298a29ea..f27d8690bac8 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -233,11 +233,16 @@ public function resolveIncludedResources(JsonApiRequest $request): array [$type, $id, $isUnique] = $value; + $relationsData = $resourceInstance->resolve($request); + $relations->push(array_filter([ 'id' => $id, 'type' => $type, '_uniqueKey' => $isUnique === true ? [$id, $type] : [$id, $type, (string) Str::random()], - 'attributes' => Arr::get($resourceInstance->resolve($request), 'data.attributes'), + 'attributes' => Arr::get($relationsData, 'data.attributes'), + 'relationships' => Arr::get($relationsData, 'data.relationships'), + 'links' => Arr::get($relationsData, 'data.links'), + 'meta' => Arr::get($relationsData, 'data.meta'), ])); } From 9f20af7b0e64f5158ddcc3248b101fabd57adc65 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Fri, 28 Nov 2025 18:18:13 +0800 Subject: [PATCH 04/48] wip Signed-off-by: Mior Muhammad Zaki --- .../Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php | 2 +- .../Http/Resources/JsonApi/JsonApiCollectionTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index f27d8690bac8..9dd62c86e26a 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -137,7 +137,7 @@ protected function resolveResourceRelationshipIdentifiers(JsonApiRequest $reques return [ ...(new Collection($this->filter($this->loadedRelationshipIdentifiers))) ->map(function ($relation) { - return ! is_null($relation) ? $relation : ['data' => []]; + return ! is_null($relation) ? $relation : ['data' => null]; })->all(), ]; } diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiCollectionTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiCollectionTest.php index f45ef5999d3d..0726a1259a0a 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiCollectionTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiCollectionTest.php @@ -107,7 +107,7 @@ public function testItCanGenerateJsonApiResponseWithRelationshipsUsingSparseIncl 'email' => $user->email, ], 'relationships' => [ - 'profile' => ['data' => []], + 'profile' => ['data' => null], 'posts' => ['data' => []], 'teams' => ['data' => []], ], From c09f42fc2c94d1a09ab2908e9c511aa09a6741a3 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Fri, 28 Nov 2025 18:26:09 +0800 Subject: [PATCH 05/48] wip Signed-off-by: Mior Muhammad Zaki --- .../Resources/JsonApi/Concerns/ResolvesJsonApiElements.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 9dd62c86e26a..8f490ffd52a4 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -154,7 +154,7 @@ protected function compileResourceRelationships(JsonApiRequest $request): void $sparseIncluded = $request->sparseIncluded(); $resourceRelationships = (new Collection($this->toRelationships($request))) - ->mapWithKeys(fn ($value, $key) => is_int($key) ? [$value => fn () => $this->resource->{$value}] : [$key => $value]) + ->mapWithKeys(fn ($value, $key) => is_int($key) ? [$value => fn ($resource) => $resource->getRelation($value)] : [$key => $value]) ->filter(fn ($value, $key) => in_array($key, $sparseIncluded)); $resourceRelationshipKeys = $resourceRelationships->keys(); @@ -164,7 +164,7 @@ protected function compileResourceRelationships(JsonApiRequest $request): void $this->loadedRelationshipsMap = new WeakMap; $this->loadedRelationshipIdentifiers = $resourceRelationships->mapWithKeys(function ($relationResolver, $key) { - $relatedModels = value($relationResolver); + $relatedModels = value($relationResolver, $this->resource); // Relationship is a collection of models... if ($relatedModels instanceof Collection) { From f701acfdd14197d51c22504fcfb9e1ca768c203c Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Mon, 1 Dec 2025 09:29:43 +0800 Subject: [PATCH 06/48] wip Signed-off-by: Mior Muhammad Zaki --- .../Concerns/ResolvesJsonApiElements.php | 7 +++--- .../Resources/JsonApi/RelationResolver.php | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 src/Illuminate/Http/Resources/JsonApi/RelationResolver.php diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 8f490ffd52a4..828074d058a5 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -12,6 +12,7 @@ use Illuminate\Http\Resources\JsonApi\Exceptions\ResourceIdentificationException; use Illuminate\Http\Resources\JsonApi\JsonApiRequest; use Illuminate\Http\Resources\JsonApi\JsonApiResource; +use Illuminate\Http\Resources\RelationResolver; use Illuminate\Http\Resources\MissingValue; use Illuminate\Support\Arr; use Illuminate\Support\Collection; @@ -154,7 +155,7 @@ protected function compileResourceRelationships(JsonApiRequest $request): void $sparseIncluded = $request->sparseIncluded(); $resourceRelationships = (new Collection($this->toRelationships($request))) - ->mapWithKeys(fn ($value, $key) => is_int($key) ? [$value => fn ($resource) => $resource->getRelation($value)] : [$key => $value]) + ->mapWithKeys(fn ($value, $key) => is_int($key) ? [$value => new RelationResolver($value)] : [$key => new RelationResolver($key, $value)]) ->filter(fn ($value, $key) => in_array($key, $sparseIncluded)); $resourceRelationshipKeys = $resourceRelationships->keys(); @@ -163,8 +164,8 @@ protected function compileResourceRelationships(JsonApiRequest $request): void $this->loadedRelationshipsMap = new WeakMap; - $this->loadedRelationshipIdentifiers = $resourceRelationships->mapWithKeys(function ($relationResolver, $key) { - $relatedModels = value($relationResolver, $this->resource); + $this->loadedRelationshipIdentifiers = $resourceRelationships->mapWithKeys(function (RelationResolver $relationResolver, $key) { + $relatedModels = $relationResolver->handle($this->resource); // Relationship is a collection of models... if ($relatedModels instanceof Collection) { diff --git a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php new file mode 100644 index 000000000000..8514cd27b47b --- /dev/null +++ b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php @@ -0,0 +1,22 @@ +relationResolver = $resolver ?? fn ($resource) => $resource->getRelation($this->relationName); + } + public function handle(mixed $resource): Collection|Model + { + return value($this->relationResolver, $resource); + } +} From 0edfd1817fe07a604aee2db7db805b0ee83c9b3d Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Mon, 1 Dec 2025 01:30:07 +0000 Subject: [PATCH 07/48] Apply fixes from StyleCI --- .../Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php | 2 +- src/Illuminate/Http/Resources/JsonApi/RelationResolver.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 828074d058a5..a9b2d50f6bb9 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -12,8 +12,8 @@ use Illuminate\Http\Resources\JsonApi\Exceptions\ResourceIdentificationException; use Illuminate\Http\Resources\JsonApi\JsonApiRequest; use Illuminate\Http\Resources\JsonApi\JsonApiResource; -use Illuminate\Http\Resources\RelationResolver; use Illuminate\Http\Resources\MissingValue; +use Illuminate\Http\Resources\RelationResolver; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Str; diff --git a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php index 8514cd27b47b..0f6b3c11fe28 100644 --- a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php +++ b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php @@ -15,6 +15,7 @@ public function __construct( ) { $this->relationResolver = $resolver ?? fn ($resource) => $resource->getRelation($this->relationName); } + public function handle(mixed $resource): Collection|Model { return value($this->relationResolver, $resource); From fcc8e439ec2ce4468586a135a9c6e6e45392b819 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Mon, 1 Dec 2025 10:27:08 +0800 Subject: [PATCH 08/48] wip Signed-off-by: Mior Muhammad Zaki --- .../Concerns/ResolvesJsonApiElements.php | 28 +++++++++++++------ .../Resources/JsonApi/RelationResolver.php | 27 ++++++++++++++++-- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index a9b2d50f6bb9..78ac9b7f5992 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -12,8 +12,8 @@ use Illuminate\Http\Resources\JsonApi\Exceptions\ResourceIdentificationException; use Illuminate\Http\Resources\JsonApi\JsonApiRequest; use Illuminate\Http\Resources\JsonApi\JsonApiResource; +use Illuminate\Http\Resources\JsonApi\RelationResolver; use Illuminate\Http\Resources\MissingValue; -use Illuminate\Http\Resources\RelationResolver; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Str; @@ -155,8 +155,11 @@ protected function compileResourceRelationships(JsonApiRequest $request): void $sparseIncluded = $request->sparseIncluded(); $resourceRelationships = (new Collection($this->toRelationships($request))) - ->mapWithKeys(fn ($value, $key) => is_int($key) ? [$value => new RelationResolver($value)] : [$key => new RelationResolver($key, $value)]) - ->filter(fn ($value, $key) => in_array($key, $sparseIncluded)); + ->mapWithKeys( + fn ($value, $key) => is_int($key) ? [$value => new RelationResolver($value)] : [$key => new RelationResolver($key, $value)] + )->filter( + fn ($value, $key) => in_array($key, $sparseIncluded) + ); $resourceRelationshipKeys = $resourceRelationships->keys(); @@ -166,6 +169,7 @@ protected function compileResourceRelationships(JsonApiRequest $request): void $this->loadedRelationshipIdentifiers = $resourceRelationships->mapWithKeys(function (RelationResolver $relationResolver, $key) { $relatedModels = $relationResolver->handle($this->resource); + $relatedResourceClass = $relationResolver->relationResourceClass; // Relationship is a collection of models... if ($relatedModels instanceof Collection) { @@ -181,11 +185,15 @@ protected function compileResourceRelationships(JsonApiRequest $request): void $key = static::resourceTypeFromModel($relatedModels->first()); - return [$key => ['data' => $relatedModels->map(function ($relation) use ($key, $isUnique) { - return transform([$key, static::resourceIdFromModel($relation)], function ($uniqueKey) use ($relation, $isUnique) { + return [$key => ['data' => $relatedModels->map(function ($relation) use ($key, $relatedResourceClass, $isUnique) { + return transform([$key, static::resourceIdFromModel($relation)], function ($uniqueKey) use ($relation, $relatedResourceClass, $isUnique) { $this->loadedRelationshipsMap[$relation] = [...$uniqueKey, $isUnique]; - return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]]; + return [ + 'id' => $uniqueKey[1], + 'type' => $uniqueKey[0], + 'class' => $relatedResourceClass, + ]; }); })]]; } @@ -202,10 +210,14 @@ protected function compileResourceRelationships(JsonApiRequest $request): void return [$key => ['data' => transform( [static::resourceTypeFromModel($relatedModel), static::resourceIdFromModel($relatedModel)], - function ($uniqueKey) use ($relatedModel) { + function ($uniqueKey) use ($relatedModel, $relatedResourceClass) { $this->loadedRelationshipsMap[$relatedModel] = [...$uniqueKey, true]; - return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]]; + return [ + 'id' => $uniqueKey[1], + 'type' => $uniqueKey[0], + 'class' => $relatedResourceClass, + ]; } )]]; })->all(); diff --git a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php index 0f6b3c11fe28..ff78ffc33ad6 100644 --- a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php +++ b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php @@ -2,18 +2,39 @@ namespace Illuminate\Http\Resources\JsonApi; -use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Collection; class RelationResolver { - public $relationResolver; + /** + * @var \Closure(mixed):(\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model) + */ + public Closure $relationResolver; + /** + * @var class-string<\Illuminate\Http\Resources\JsonApi\JsonApiResource>|null + */ + public ?string $relationResourceClass; + + /** + * Construct a new resource relationship resolver. + * + * @param string $relationName + * @param \Closure(mixed):(\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model)|class-string<\Illuminate\Http\Resources\JsonApi\JsonApiResource>|null $resolver + */ public function __construct( public string $relationName, Closure|string|null $resolver = null ) { - $this->relationResolver = $resolver ?? fn ($resource) => $resource->getRelation($this->relationName); + $this->relationResolver = match (true) { + $resolver instanceof Closure => $resolver, + default => fn ($resource) => $resource->getRelation($this->relationName), + }; + + if (is_string($resolver) && class_exists($resolver)) { + $this->relationResourceClass = $resolver; + } } public function handle(mixed $resource): Collection|Model From af659e8d389ca667041a14c173994324a5545a55 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Mon, 1 Dec 2025 02:27:22 +0000 Subject: [PATCH 09/48] Apply fixes from StyleCI --- src/Illuminate/Http/Resources/JsonApi/RelationResolver.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php index ff78ffc33ad6..3aac5dca1ebf 100644 --- a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php +++ b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php @@ -20,8 +20,8 @@ class RelationResolver /** * Construct a new resource relationship resolver. * - * @param string $relationName - * @param \Closure(mixed):(\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model)|class-string<\Illuminate\Http\Resources\JsonApi\JsonApiResource>|null $resolver + * @param string $relationName + * @param \Closure(mixed):(\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model)|class-string<\Illuminate\Http\Resources\JsonApi\JsonApiResource>|null $resolver */ public function __construct( public string $relationName, From 54d579b6618651185b9e6efbcdde5460ddbb090c Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Mon, 1 Dec 2025 10:49:34 +0800 Subject: [PATCH 10/48] wip Signed-off-by: Mior Muhammad Zaki --- .../JsonApi/Concerns/ResolvesJsonApiElements.php | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 78ac9b7f5992..e1f278723945 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -187,12 +187,11 @@ protected function compileResourceRelationships(JsonApiRequest $request): void return [$key => ['data' => $relatedModels->map(function ($relation) use ($key, $relatedResourceClass, $isUnique) { return transform([$key, static::resourceIdFromModel($relation)], function ($uniqueKey) use ($relation, $relatedResourceClass, $isUnique) { - $this->loadedRelationshipsMap[$relation] = [...$uniqueKey, $isUnique]; + $this->loadedRelationshipsMap[$relation] = [...$uniqueKey, $relatedResourceClass, $isUnique]; return [ 'id' => $uniqueKey[1], 'type' => $uniqueKey[0], - 'class' => $relatedResourceClass, ]; }); })]]; @@ -211,12 +210,11 @@ protected function compileResourceRelationships(JsonApiRequest $request): void return [$key => ['data' => transform( [static::resourceTypeFromModel($relatedModel), static::resourceIdFromModel($relatedModel)], function ($uniqueKey) use ($relatedModel, $relatedResourceClass) { - $this->loadedRelationshipsMap[$relatedModel] = [...$uniqueKey, true]; + $this->loadedRelationshipsMap[$relatedModel] = [...$uniqueKey, $relatedResourceClass, true]; return [ 'id' => $uniqueKey[1], 'type' => $uniqueKey[0], - 'class' => $relatedResourceClass, ]; } )]]; @@ -237,15 +235,15 @@ public function resolveIncludedResources(JsonApiRequest $request): array $relations = new Collection; foreach ($this->loadedRelationshipsMap as $relation => $value) { - $resourceInstance = rescue(fn () => $relation->toResource(), new JsonApiResource($relation), false); + [$type, $id, $relatedResourceClass, $isUnique] = $value; + + $resourceInstance = rescue(fn () => $relation->toResource($relatedResourceClass), new JsonApiResource($relation), false); if (! $resourceInstance instanceof JsonApiResource && $resourceInstance instanceof JsonResource) { $resourceInstance = new JsonApiResource($resourceInstance->resource); } - [$type, $id, $isUnique] = $value; - $relationsData = $resourceInstance->resolve($request); $relations->push(array_filter([ From a524678389572676b63b926e072656c94ee2f0bd Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Mon, 1 Dec 2025 10:59:22 +0800 Subject: [PATCH 11/48] wip Signed-off-by: Mior Muhammad Zaki --- src/Illuminate/Http/Resources/JsonApi/RelationResolver.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php index 3aac5dca1ebf..ce0a38239a82 100644 --- a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php +++ b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php @@ -2,8 +2,9 @@ namespace Illuminate\Http\Resources\JsonApi; +use Closure; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Collection; class RelationResolver { From 84a230c486d89a9ef30dbbed657bb5ee88a419bb Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Mon, 1 Dec 2025 11:00:19 +0800 Subject: [PATCH 12/48] wip Signed-off-by: Mior Muhammad Zaki --- src/Illuminate/Http/Resources/JsonApi/RelationResolver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php index ce0a38239a82..3377a0a08cce 100644 --- a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php +++ b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php @@ -16,7 +16,7 @@ class RelationResolver /** * @var class-string<\Illuminate\Http\Resources\JsonApi\JsonApiResource>|null */ - public ?string $relationResourceClass; + public ?string $relationResourceClass = null; /** * Construct a new resource relationship resolver. From c7c265f90cef038040145bf1d10b1917ff7a1a95 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Mon, 1 Dec 2025 11:02:05 +0800 Subject: [PATCH 13/48] wip Signed-off-by: Mior Muhammad Zaki --- src/Illuminate/Http/Resources/JsonApi/RelationResolver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php index 3377a0a08cce..aa777fdbef7c 100644 --- a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php +++ b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php @@ -38,7 +38,7 @@ public function __construct( } } - public function handle(mixed $resource): Collection|Model + public function handle(mixed $resource): Collection|Model|null { return value($this->relationResolver, $resource); } From 496ce0b1be72fc3fe99d4530c3b7f32de483530b Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Mon, 1 Dec 2025 11:04:45 +0800 Subject: [PATCH 14/48] wip Signed-off-by: Mior Muhammad Zaki --- src/Illuminate/Http/Resources/JsonApi/RelationResolver.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php index aa777fdbef7c..2f6270675e56 100644 --- a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php +++ b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php @@ -6,6 +6,9 @@ use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; +/** + * @internal + */ class RelationResolver { /** @@ -21,7 +24,6 @@ class RelationResolver /** * Construct a new resource relationship resolver. * - * @param string $relationName * @param \Closure(mixed):(\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model)|class-string<\Illuminate\Http\Resources\JsonApi\JsonApiResource>|null $resolver */ public function __construct( @@ -38,6 +40,9 @@ public function __construct( } } + /** + * Resolve relation for a resource. + */ public function handle(mixed $resource): Collection|Model|null { return value($this->relationResolver, $resource); From 46ee3b9a5a4a04437bbe1a3465d7f1bca4cd10c7 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Mon, 1 Dec 2025 12:08:46 +0800 Subject: [PATCH 15/48] wip Signed-off-by: Mior Muhammad Zaki --- src/Illuminate/Http/Resources/JsonApi/RelationResolver.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php index 2f6270675e56..2288aa69c01e 100644 --- a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php +++ b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php @@ -12,7 +12,7 @@ class RelationResolver { /** - * @var \Closure(mixed):(\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model) + * @var \Closure(mixed):(\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model|null) */ public Closure $relationResolver; @@ -24,7 +24,7 @@ class RelationResolver /** * Construct a new resource relationship resolver. * - * @param \Closure(mixed):(\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model)|class-string<\Illuminate\Http\Resources\JsonApi\JsonApiResource>|null $resolver + * @param \Closure(mixed):(\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model|null)|class-string<\Illuminate\Http\Resources\JsonApi\JsonApiResource>|null $resolver */ public function __construct( public string $relationName, From e4be683058d10023f716a7d88241de55afbebd43 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Mon, 1 Dec 2025 12:34:58 +0800 Subject: [PATCH 16/48] wip Signed-off-by: Mior Muhammad Zaki --- .../JsonApi/Fixtures/AuthorApiResource.php | 21 +++++++++++++++++++ .../Http/Resources/JsonApi/Fixtures/Post.php | 2 +- .../JsonApi/Fixtures/PostApiResource.php | 4 ++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 tests/Integration/Http/Resources/JsonApi/Fixtures/AuthorApiResource.php diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/AuthorApiResource.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/AuthorApiResource.php new file mode 100644 index 000000000000..1ce39362b057 --- /dev/null +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/AuthorApiResource.php @@ -0,0 +1,21 @@ + $this->name, + 'email' => $this->email, + ]; + } +} diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/Post.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/Post.php index 143758e2c966..3f035bbd3c5f 100644 --- a/tests/Integration/Http/Resources/JsonApi/Fixtures/Post.php +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/Post.php @@ -13,7 +13,7 @@ class Post extends Model { use HasFactory; - public function user() + public function author() { return $this->belongsTo(User::class); } diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/PostApiResource.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/PostApiResource.php index 6309e6cc5c91..dd847d304c5b 100644 --- a/tests/Integration/Http/Resources/JsonApi/Fixtures/PostApiResource.php +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/PostApiResource.php @@ -10,4 +10,8 @@ class PostApiResource extends JsonApiResource 'title', 'content', ]; + + protected array $relationships = [ + 'author' => AuthorApiResource::class, + ]; } From e28661c2cba114faacc1bf6e37eb1ec6910a9798 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Mon, 1 Dec 2025 12:40:01 +0800 Subject: [PATCH 17/48] wip Signed-off-by: Mior Muhammad Zaki --- .../Http/Resources/JsonApi/Fixtures/AuthorApiResource.php | 7 +++++++ .../Http/Resources/JsonApi/Fixtures/UserApiResource.php | 1 + .../Http/Resources/JsonApi/Fixtures/UserResource.php | 1 + 3 files changed, 9 insertions(+) diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/AuthorApiResource.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/AuthorApiResource.php index 1ce39362b057..0f8f7403cedc 100644 --- a/tests/Integration/Http/Resources/JsonApi/Fixtures/AuthorApiResource.php +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/AuthorApiResource.php @@ -11,6 +11,7 @@ class AuthorApiResource extends JsonApiResource 'profile', ]; + #[\Override] public function toAttributes(Request $request) { return [ @@ -18,4 +19,10 @@ public function toAttributes(Request $request) 'email' => $this->email, ]; } + + #[\Override] + public function toType(Request $request) + { + return 'authors'; + } } diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/UserApiResource.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/UserApiResource.php index 7d1a7c7918c5..a45a8b37fa78 100644 --- a/tests/Integration/Http/Resources/JsonApi/Fixtures/UserApiResource.php +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/UserApiResource.php @@ -13,6 +13,7 @@ class UserApiResource extends JsonApiResource 'teams', ]; + #[\Override] public function toAttributes(Request $request) { return [ diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/UserResource.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/UserResource.php index 7ddd2248a37b..05b5e843fccd 100644 --- a/tests/Integration/Http/Resources/JsonApi/Fixtures/UserResource.php +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/UserResource.php @@ -7,6 +7,7 @@ class UserResource extends JsonResource { + #[\Override] public function toArray(Request $request) { return [ From 302b3b12adb83fb9d65426caa195b04a7cf420ff Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Tue, 2 Dec 2025 09:32:25 +0800 Subject: [PATCH 18/48] wip Signed-off-by: Mior Muhammad Zaki --- .../Concerns/ResolvesJsonApiElements.php | 13 ++++++------ .../Resources/JsonApi/RelationResolver.php | 21 +++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index e1f278723945..0b34cbb19034 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -167,9 +167,10 @@ protected function compileResourceRelationships(JsonApiRequest $request): void $this->loadedRelationshipsMap = new WeakMap; - $this->loadedRelationshipIdentifiers = $resourceRelationships->mapWithKeys(function (RelationResolver $relationResolver, $key) { + $this->loadedRelationshipIdentifiers = $resourceRelationships->mapWithKeys(function (RelationResolver $relationResolver, $key) use ($request) { $relatedModels = $relationResolver->handle($this->resource); - $relatedResourceClass = $relationResolver->relationResourceClass; + $relatedResourceType = $relationResolver->resourceType($relatedModels, $request); + $relatedResourceClass = $relationResolver->resourceClass(); // Relationship is a collection of models... if ($relatedModels instanceof Collection) { @@ -183,7 +184,7 @@ protected function compileResourceRelationships(JsonApiRequest $request): void $isUnique = ! $relationship instanceof BelongsToMany; - $key = static::resourceTypeFromModel($relatedModels->first()); + $key = $relatedResourceType ?? static::resourceTypeFromModel($relatedModels->first()); return [$key => ['data' => $relatedModels->map(function ($relation) use ($key, $relatedResourceClass, $isUnique) { return transform([$key, static::resourceIdFromModel($relation)], function ($uniqueKey) use ($relation, $relatedResourceClass, $isUnique) { @@ -208,7 +209,7 @@ protected function compileResourceRelationships(JsonApiRequest $request): void } return [$key => ['data' => transform( - [static::resourceTypeFromModel($relatedModel), static::resourceIdFromModel($relatedModel)], + [$relatedResourceType ?? static::resourceTypeFromModel($relatedModel), static::resourceIdFromModel($relatedModel)], function ($uniqueKey) use ($relatedModel, $relatedResourceClass) { $this->loadedRelationshipsMap[$relatedModel] = [...$uniqueKey, $relatedResourceClass, true]; @@ -285,7 +286,7 @@ protected function resolveResourceMetaInformation(JsonApiRequest $request): arra /** * Get the resource ID from the given Eloquent model. */ - protected static function resourceIdFromModel(Model $model): string + public static function resourceIdFromModel(Model $model): string { return $model->getKey(); } @@ -293,7 +294,7 @@ protected static function resourceIdFromModel(Model $model): string /** * Get the resource type from the given Eloquent model. */ - protected static function resourceTypeFromModel(Model $model): string + public static function resourceTypeFromModel(Model $model): string { $modelClassName = $model::class; diff --git a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php index 2288aa69c01e..9c20dae58122 100644 --- a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php +++ b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php @@ -47,4 +47,25 @@ public function handle(mixed $resource): Collection|Model|null { return value($this->relationResolver, $resource); } + + /** + * Get the resource class. + * + * @return class-string<\Illuminate\Http\Resources\JsonApi\JsonApiResource>|null + */ + public function resourceClass(): ?string + { + return $this->relationResourceClass; + } + + public function resourceType(Collection|Model|null $resources, JsonApiRequest $request): ?string + { + if (is_null($resourceClass = $this->resourceClass())) { + return null; + } + + $resource = $resources instanceof Collection ? $resources->first() : $resources; + + return (new $resourceClass($resource))->toType($request); + } } From 92e5d4dd51d54144f03d1d39cbe46e2748452a1f Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Tue, 2 Dec 2025 09:43:27 +0800 Subject: [PATCH 19/48] wip Signed-off-by: Mior Muhammad Zaki --- .../JsonApi/Concerns/ResolvesJsonApiElements.php | 5 ++--- .../Http/Resources/JsonApi/RelationResolver.php | 8 +++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 0b34cbb19034..5d82d38c1656 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -169,7 +169,6 @@ protected function compileResourceRelationships(JsonApiRequest $request): void $this->loadedRelationshipIdentifiers = $resourceRelationships->mapWithKeys(function (RelationResolver $relationResolver, $key) use ($request) { $relatedModels = $relationResolver->handle($this->resource); - $relatedResourceType = $relationResolver->resourceType($relatedModels, $request); $relatedResourceClass = $relationResolver->resourceClass(); // Relationship is a collection of models... @@ -184,7 +183,7 @@ protected function compileResourceRelationships(JsonApiRequest $request): void $isUnique = ! $relationship instanceof BelongsToMany; - $key = $relatedResourceType ?? static::resourceTypeFromModel($relatedModels->first()); + $key = $relationResolver->resourceType($relatedModels, $request); return [$key => ['data' => $relatedModels->map(function ($relation) use ($key, $relatedResourceClass, $isUnique) { return transform([$key, static::resourceIdFromModel($relation)], function ($uniqueKey) use ($relation, $relatedResourceClass, $isUnique) { @@ -209,7 +208,7 @@ protected function compileResourceRelationships(JsonApiRequest $request): void } return [$key => ['data' => transform( - [$relatedResourceType ?? static::resourceTypeFromModel($relatedModel), static::resourceIdFromModel($relatedModel)], + [$relationResolver->resourceType($relatedModel, $request), static::resourceIdFromModel($relatedModel)], function ($uniqueKey) use ($relatedModel, $relatedResourceClass) { $this->loadedRelationshipsMap[$relatedModel] = [...$uniqueKey, $relatedResourceClass, true]; diff --git a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php index 9c20dae58122..c3f67668188f 100644 --- a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php +++ b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php @@ -60,12 +60,14 @@ public function resourceClass(): ?string public function resourceType(Collection|Model|null $resources, JsonApiRequest $request): ?string { - if (is_null($resourceClass = $this->resourceClass())) { + $resource = $resources instanceof Collection ? $resources->first() : $resources; + + if (is_null($resource)) { return null; + } elseif (is_null($resourceClass = $this->resourceClass())) { + return JsonApiResource::resourceTypeFromModel($resource); } - $resource = $resources instanceof Collection ? $resources->first() : $resources; - return (new $resourceClass($resource))->toType($request); } } From 7f0b513ebd65a95b184ac27108997f4c3a857e6e Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Tue, 2 Dec 2025 09:48:20 +0800 Subject: [PATCH 20/48] wip Signed-off-by: Mior Muhammad Zaki --- tests/Integration/Http/Resources/JsonApi/Fixtures/Post.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/Post.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/Post.php index 3f035bbd3c5f..ee4129099a68 100644 --- a/tests/Integration/Http/Resources/JsonApi/Fixtures/Post.php +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/Post.php @@ -15,6 +15,6 @@ class Post extends Model public function author() { - return $this->belongsTo(User::class); + return $this->belongsTo(User::class, 'user_id'); } } From 7af5ced11cf6bc2fa9877a4fb33398a6094a9e82 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Tue, 2 Dec 2025 09:52:52 +0800 Subject: [PATCH 21/48] wip Signed-off-by: Mior Muhammad Zaki --- src/Illuminate/Http/Resources/JsonApi/RelationResolver.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php index c3f67668188f..88a23dd04d27 100644 --- a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php +++ b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php @@ -58,6 +58,9 @@ public function resourceClass(): ?string return $this->relationResourceClass; } + /** + * Get the relation resource type. + */ public function resourceType(Collection|Model|null $resources, JsonApiRequest $request): ?string { $resource = $resources instanceof Collection ? $resources->first() : $resources; From 29d6aa21919baa312a79504b86dfd8d5dca065e7 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Tue, 2 Dec 2025 10:04:06 +0800 Subject: [PATCH 22/48] wip Signed-off-by: Mior Muhammad Zaki --- src/Illuminate/Http/Resources/JsonApi/RelationResolver.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php index 88a23dd04d27..61be9d9bf646 100644 --- a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php +++ b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php @@ -61,13 +61,11 @@ public function resourceClass(): ?string /** * Get the relation resource type. */ - public function resourceType(Collection|Model|null $resources, JsonApiRequest $request): ?string + public function resourceType(Collection|Model $resources, JsonApiRequest $request): ?string { $resource = $resources instanceof Collection ? $resources->first() : $resources; - if (is_null($resource)) { - return null; - } elseif (is_null($resourceClass = $this->resourceClass())) { + if (is_null($resourceClass = $this->resourceClass())) { return JsonApiResource::resourceTypeFromModel($resource); } From 15f56cd64ff0472731f80ea88b4cd21aea957248 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Tue, 2 Dec 2025 13:51:38 +0800 Subject: [PATCH 23/48] wip Signed-off-by: Mior Muhammad Zaki --- src/Illuminate/Http/Resources/JsonApi/RelationResolver.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php index 61be9d9bf646..098d20ef6a6c 100644 --- a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php +++ b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php @@ -12,11 +12,15 @@ class RelationResolver { /** + * The relation resolver. + * * @var \Closure(mixed):(\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model|null) */ public Closure $relationResolver; /** + * The relation resource class. + * * @var class-string<\Illuminate\Http\Resources\JsonApi\JsonApiResource>|null */ public ?string $relationResourceClass = null; From b33b40b65164d0f9d72900b0c93d4549d54d128b Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Tue, 2 Dec 2025 16:36:06 +0800 Subject: [PATCH 24/48] wip Signed-off-by: Mior Muhammad Zaki --- .../Http/Resources/JsonApi/RelationResolver.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php index 098d20ef6a6c..9837f8292ed2 100644 --- a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php +++ b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php @@ -5,6 +5,7 @@ use Closure; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; +use Illuminate\Http\Resources\JsonApi\Exceptions\ResourceIdentificationException; /** * @internal @@ -73,6 +74,13 @@ public function resourceType(Collection|Model $resources, JsonApiRequest $reques return JsonApiResource::resourceTypeFromModel($resource); } - return (new $resourceClass($resource))->toType($request); + $relatedResource = new $resourceClass($resource); + + return tap($relatedResource->toType($request), function ($resourceType) use ($relatedResource) { + throw_if( + is_null($resourceType), + ResourceIdentificationException::attemptingToDetermineTypeFor($relatedResource) + ); + }); } } From 6f528759f2400d547c0d8427887ee67931489946 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Tue, 2 Dec 2025 20:15:09 +0800 Subject: [PATCH 25/48] wip Signed-off-by: Mior Muhammad Zaki --- .../JsonApi/Fixtures/AuthorApiResource.php | 1 + .../Resources/JsonApi/Fixtures/Comment.php | 23 ++++++++ .../JsonApi/Fixtures/CommentApiResource.php | 24 ++++++++ .../JsonApi/Fixtures/CommentFactory.php | 18 ++++++ .../Http/Resources/JsonApi/Fixtures/Post.php | 5 ++ .../Http/Resources/JsonApi/Fixtures/User.php | 5 ++ .../JsonApi/Fixtures/UserApiResource.php | 1 + .../Resources/JsonApi/Fixtures/migrations.php | 8 +++ .../Resources/JsonApi/JsonApiResourceTest.php | 55 +++++++++++++++++++ .../Http/Resources/JsonApi/TestCase.php | 9 +++ 10 files changed, 149 insertions(+) create mode 100644 tests/Integration/Http/Resources/JsonApi/Fixtures/Comment.php create mode 100644 tests/Integration/Http/Resources/JsonApi/Fixtures/CommentApiResource.php create mode 100644 tests/Integration/Http/Resources/JsonApi/Fixtures/CommentFactory.php diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/AuthorApiResource.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/AuthorApiResource.php index 0f8f7403cedc..227723bbc1b2 100644 --- a/tests/Integration/Http/Resources/JsonApi/Fixtures/AuthorApiResource.php +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/AuthorApiResource.php @@ -8,6 +8,7 @@ class AuthorApiResource extends JsonApiResource { protected array $relationships = [ + 'comments', 'profile', ]; diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/Comment.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/Comment.php new file mode 100644 index 000000000000..05a516624d4b --- /dev/null +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/Comment.php @@ -0,0 +1,23 @@ +belongsTo(Post::class); + } + + public function commenter() + { + return $this->belongsTo(User::class, 'user_id'); + } +} diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/CommentApiResource.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/CommentApiResource.php new file mode 100644 index 000000000000..8302d235113e --- /dev/null +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/CommentApiResource.php @@ -0,0 +1,24 @@ + UserApiResource::class, + ]; +} diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/CommentFactory.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/CommentFactory.php new file mode 100644 index 000000000000..3d6c26308cf7 --- /dev/null +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/CommentFactory.php @@ -0,0 +1,18 @@ + PostFactory::new(), + 'user_id' => UserFactory::new(), + 'content' => $this->faker->words(10, true), + ]; + } +} diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/Post.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/Post.php index ee4129099a68..1c583b9ce9cb 100644 --- a/tests/Integration/Http/Resources/JsonApi/Fixtures/Post.php +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/Post.php @@ -13,6 +13,11 @@ class Post extends Model { use HasFactory; + public function comments() + { + return $this->hasMany(Comment::class); + } + public function author() { return $this->belongsTo(User::class, 'user_id'); diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/User.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/User.php index ef7cac2097a2..a7adc43ba121 100644 --- a/tests/Integration/Http/Resources/JsonApi/Fixtures/User.php +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/User.php @@ -24,6 +24,11 @@ public function posts() return $this->hasMany(Post::class); } + public function comments() + { + return $this->hasMany(Comment::class); + } + public function teams() { return $this->belongsToMany(Team::class) diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/UserApiResource.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/UserApiResource.php index a45a8b37fa78..bce50ff6da07 100644 --- a/tests/Integration/Http/Resources/JsonApi/Fixtures/UserApiResource.php +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/UserApiResource.php @@ -8,6 +8,7 @@ class UserApiResource extends JsonApiResource { protected array $relationships = [ + 'comments', 'profile', 'posts', 'teams', diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/migrations.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/migrations.php index e560a3eb970a..26afadca7cb9 100644 --- a/tests/Integration/Http/Resources/JsonApi/Fixtures/migrations.php +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/migrations.php @@ -34,3 +34,11 @@ $table->index(['team_id', 'user_id']); }); + +Schema::create('comments', function (Blueprint $table) { + $table->id(); + $table->foreignId('post_id')->unique(); + $table->foreignId('user_id')->index()->nullable(); + $table->text('content'); + $table->timestamps(); +}); diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index 1ba46b92810a..9b4160b3d75b 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Integration\Http\Resources\JsonApi; +use Illuminate\Tests\Integration\Http\Resources\JsonApi\Fixtures\Comment; use Illuminate\Tests\Integration\Http\Resources\JsonApi\Fixtures\Post; use Illuminate\Tests\Integration\Http\Resources\JsonApi\Fixtures\Profile; use Illuminate\Tests\Integration\Http\Resources\JsonApi\Fixtures\Team; @@ -188,4 +189,58 @@ public function testItCanGenerateJsonApiResponseWithRelationshipsUsingSparseIncl ], ]); } + + public function test_it_can_resolve_relationship_with_custom_name_and_resource_class() + { + $now = $this->freezeSecond(); + + $user = User::factory()->create(); + + $profile = Profile::factory()->create([ + 'user_id' => $user->getKey(), + 'date_of_birth' => '2011-06-09', + 'timezone' => 'America/Chicago', + ]); + + [$post1, $post2] = Post::factory()->times(2)->create([ + 'user_id' => $user->getKey(), + ]); + + $comments = Comment::factory()->create([ + 'post_id' => $post1->getKey(), + 'user_id' => $user->getKey(), + ]); + + $this->getJson("/posts/{$post1->getKey()}?".http_build_query(['include' => 'author'])) + ->assertHeader('Content-type', 'application/vnd.api+json') + ->assertExactJson([ + 'data' => [ + 'attributes' => [ + 'content' => $post1->content, + 'title' => $post1->title, + ], + 'type' => 'posts', + 'id' => (string) $post1->getKey(), + 'relationships' => [ + 'author' => [ + 'data' => [ + 'id' => (string) $user->getKey(), + 'type' => 'authors', + ], + ] + ] + ], + 'included' => [ + [ + 'attributes' => [ + 'email' => $user->email, + 'name' => $user->name, + ], + 'id' => (string) $user->getKey(), + 'type' => 'authors', + ], + ], + ]) + ->assertJsonMissing(['jsonapi']); + } } diff --git a/tests/Integration/Http/Resources/JsonApi/TestCase.php b/tests/Integration/Http/Resources/JsonApi/TestCase.php index a1644383a7cb..4904345733cc 100644 --- a/tests/Integration/Http/Resources/JsonApi/TestCase.php +++ b/tests/Integration/Http/Resources/JsonApi/TestCase.php @@ -5,6 +5,7 @@ use Illuminate\Foundation\Testing\LazilyRefreshDatabase; use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\JsonApi\JsonApiResource; +use Illuminate\Tests\Integration\Http\Resources\JsonApi\Fixtures\Post; use Illuminate\Tests\Integration\Http\Resources\JsonApi\Fixtures\User; use Orchestra\Testbench\Attributes\WithConfig; use Orchestra\Testbench\Attributes\WithMigration; @@ -36,6 +37,14 @@ protected function defineRoutes($router) $router->get('users/{userId}', function ($userId) { return User::find($userId)->toResource(); }); + + $router->get('posts', function () { + return Post::paginate(5)->toResourceCollection(); + }); + + $router->get('posts/{postId}', function ($postId) { + return Post::find($postId)->toResource(); + }); } /** {@inheritdoc} */ From c93256ea7abb8d13459f87fff7dd49b0bed2c188 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Tue, 2 Dec 2025 12:15:40 +0000 Subject: [PATCH 26/48] Apply fixes from StyleCI --- .../Http/Resources/JsonApi/Fixtures/CommentApiResource.php | 1 - .../Http/Resources/JsonApi/JsonApiResourceTest.php | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/CommentApiResource.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/CommentApiResource.php index 8302d235113e..d874417ebd30 100644 --- a/tests/Integration/Http/Resources/JsonApi/Fixtures/CommentApiResource.php +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/CommentApiResource.php @@ -2,7 +2,6 @@ namespace Illuminate\Tests\Integration\Http\Resources\JsonApi\Fixtures; -use Illuminate\Http\Request; use Illuminate\Http\Resources\JsonApi\JsonApiResource; class CommentApiResource extends JsonApiResource diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index 9b4160b3d75b..59f26d849f37 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -227,8 +227,8 @@ public function test_it_can_resolve_relationship_with_custom_name_and_resource_c 'id' => (string) $user->getKey(), 'type' => 'authors', ], - ] - ] + ], + ], ], 'included' => [ [ From ac5c68e0333b276368cea7fd5fe2f7d90a362428 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 3 Dec 2025 10:26:06 +0800 Subject: [PATCH 27/48] wip Signed-off-by: Mior Muhammad Zaki --- src/Illuminate/Http/Resources/JsonApi/RelationResolver.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php index 9837f8292ed2..47b775df6cfb 100644 --- a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php +++ b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php @@ -65,6 +65,8 @@ public function resourceClass(): ?string /** * Get the relation resource type. + * + * @throws \Illuminate\Http\Resources\JsonApi\Exceptions\ResourceIdentificationException */ public function resourceType(Collection|Model $resources, JsonApiRequest $request): ?string { From c5a50bc746ecc44f2aef4975ef97c368d81ccfdb Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Tue, 2 Dec 2025 20:31:27 +0800 Subject: [PATCH 28/48] wip Signed-off-by: Mior Muhammad Zaki --- .../Http/Resources/Json/JsonResource.php | 11 +++++++++++ .../Http/Resources/JsonApi/JsonApiResource.php | 14 +++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/Json/JsonResource.php b/src/Illuminate/Http/Resources/Json/JsonResource.php index 02ebce156d8c..16bb7a0ea00c 100644 --- a/src/Illuminate/Http/Resources/Json/JsonResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonResource.php @@ -131,6 +131,17 @@ public function resolve($request = null) * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable */ public function toAttributes(Request $request) + { + return $this->resolveResourceDataToArray($request); + } + + /** + * Resolve the resource data to an array. + * + * @param \Illuminate\Http\Request $request + * @return array + */ + public function resolveResourceDataToArray(Request $request) { return $this->toArray($request); } diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index 003ffa41a6d1..2cfb19f506d6 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -149,10 +149,22 @@ public function with($request) public function resolve($request = null) { return [ - 'data' => $this->resolveResourceData($this->resolveJsonApiRequestFrom($request ?? $this->resolveRequestFromContainer())), + 'data' => $this->resolveResourceDataToArray($this->resolveJsonApiRequestFrom($request ?? $this->resolveRequestFromContainer())), ]; } + /** + * Resolve the resource data to an array. + * + * @param \Illuminate\Http\Request $request + * @return array + */ + #[\Override] + public function resolveResourceDataToArray(Request $request) + { + return $this->resolveResourceData($request); + } + /** * Customize the outgoing response for the resource. */ From ade3d72c47bc0d5a9007d3d1fd38c38918479621 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 3 Dec 2025 11:44:12 +0800 Subject: [PATCH 29/48] wip Signed-off-by: Mior Muhammad Zaki --- src/Illuminate/Http/Resources/Json/JsonResource.php | 4 ++-- .../Resources/JsonApi/Concerns/ResolvesJsonApiElements.php | 2 +- src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Illuminate/Http/Resources/Json/JsonResource.php b/src/Illuminate/Http/Resources/Json/JsonResource.php index 16bb7a0ea00c..33c02cbc7b14 100644 --- a/src/Illuminate/Http/Resources/Json/JsonResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonResource.php @@ -132,7 +132,7 @@ public function resolve($request = null) */ public function toAttributes(Request $request) { - return $this->resolveResourceDataToArray($request); + return $this->resolveResourceData($request); } /** @@ -141,7 +141,7 @@ public function toAttributes(Request $request) * @param \Illuminate\Http\Request $request * @return array */ - public function resolveResourceDataToArray(Request $request) + public function resolveResourceData(Request $request) { return $this->toArray($request); } diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 5d82d38c1656..d13097cfcf66 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -37,7 +37,7 @@ trait ResolvesJsonApiElements /** * Resolves `data` for the resource. */ - public function resolveResourceData(JsonApiRequest $request): array + public function resolveJsonApiResourceObject(JsonApiRequest $request): array { $resourceType = $this->resolveResourceType($request); diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index 2cfb19f506d6..b5b1f34f6c72 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -149,7 +149,7 @@ public function with($request) public function resolve($request = null) { return [ - 'data' => $this->resolveResourceDataToArray($this->resolveJsonApiRequestFrom($request ?? $this->resolveRequestFromContainer())), + 'data' => $this->resolveResourceData($this->resolveJsonApiRequestFrom($request ?? $this->resolveRequestFromContainer())), ]; } @@ -160,9 +160,9 @@ public function resolve($request = null) * @return array */ #[\Override] - public function resolveResourceDataToArray(Request $request) + public function resolveResourceData(Request $request) { - return $this->resolveResourceData($request); + return $this->resolveJsonApiResourceObject($request); } /** From 02939748d68ad5054c485960f89f83cf097b3f24 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 3 Dec 2025 12:04:56 +0800 Subject: [PATCH 30/48] wip Signed-off-by: Mior Muhammad Zaki --- .../Concerns/ResolvesJsonApiElements.php | 31 ++++++++++++++++--- .../Resources/JsonApi/JsonApiResource.php | 2 +- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index d13097cfcf66..8ec4f8fd0240 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -22,6 +22,11 @@ trait ResolvesJsonApiElements { + /** + * Determine whether resource use included and fields from the request object. + */ + protected bool $usesRequestQueryString = true; + /** * Cached loaded relationships map. * @@ -34,10 +39,22 @@ trait ResolvesJsonApiElements */ protected array $loadedRelationshipIdentifiers = []; + public function withRequestQueryString(bool $value = true) + { + $this->usesRequestQueryString = $value; + + return $this; + } + + public function withoutRequestQueryString() + { + return $this->withRequestQueryString(false); + } + /** * Resolves `data` for the resource. */ - public function resolveJsonApiResourceObject(JsonApiRequest $request): array + protected function resolveResourceObject(JsonApiRequest $request): array { $resourceType = $this->resolveResourceType($request); @@ -108,7 +125,10 @@ protected function resolveResourceAttributes(JsonApiRequest $request, string $re $data = $data->jsonSerialize(); } - $sparseFieldset = $request->sparseFields($resourceType); + $sparseFieldset = match ($this->usesRequestQueryString) { + true => $request->sparseFields($resourceType), + default => [], + }; $data = (new Collection($data)) ->mapWithKeys(fn ($value, $key) => is_int($key) ? [$value => $this->resource->{$value}] : [$key => $value]) @@ -152,7 +172,10 @@ protected function compileResourceRelationships(JsonApiRequest $request): void return; } - $sparseIncluded = $request->sparseIncluded(); + $sparseIncluded = match ($this->usesRequestQueryString) { + true => $request->sparseIncluded(), + default => [], + }; $resourceRelationships = (new Collection($this->toRelationships($request))) ->mapWithKeys( @@ -244,7 +267,7 @@ public function resolveIncludedResources(JsonApiRequest $request): array $resourceInstance = new JsonApiResource($resourceInstance->resource); } - $relationsData = $resourceInstance->resolve($request); + $relationsData = $resourceInstance->withoutRequestQueryString()->resolve($request); $relations->push(array_filter([ 'id' => $id, diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index b5b1f34f6c72..e764132d197e 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -162,7 +162,7 @@ public function resolve($request = null) #[\Override] public function resolveResourceData(Request $request) { - return $this->resolveJsonApiResourceObject($request); + return $this->resolveResourceObject($request); } /** From 7f4b3f086ecc770effdfdbc2804f8fd4143fbd2d Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 3 Dec 2025 12:47:20 +0800 Subject: [PATCH 31/48] wip Signed-off-by: Mior Muhammad Zaki --- .../Concerns/ResolvesJsonApiElements.php | 29 ++++++--- .../Resources/JsonApi/JsonApiResourceTest.php | 61 ++++++++++++++++++- 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 8ec4f8fd0240..af156aca515f 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -27,6 +27,11 @@ trait ResolvesJsonApiElements */ protected bool $usesRequestQueryString = true; + /** + * Determine whether included relationship for the resource from eager loaded relationship. + */ + protected bool $usesIncludedFromLoadedRelationships = false; + /** * Cached loaded relationships map. * @@ -51,6 +56,13 @@ public function withoutRequestQueryString() return $this->withRequestQueryString(false); } + public function withIncludedFromLoadedRelationship() + { + $this->usesIncludedFromLoadedRelationships = true; + + return $this; + } + /** * Resolves `data` for the resource. */ @@ -172,17 +184,18 @@ protected function compileResourceRelationships(JsonApiRequest $request): void return; } - $sparseIncluded = match ($this->usesRequestQueryString) { - true => $request->sparseIncluded(), + $sparseIncluded = match (true) { + $this->usesRequestQueryString === true => $request->sparseIncluded(), + $this->usesRequestQueryString === false && $this->usesIncludedFromLoadedRelationships === true => array_keys($this->resource->getRelations()), default => [], }; $resourceRelationships = (new Collection($this->toRelationships($request))) - ->mapWithKeys( - fn ($value, $key) => is_int($key) ? [$value => new RelationResolver($value)] : [$key => new RelationResolver($key, $value)] - )->filter( - fn ($value, $key) => in_array($key, $sparseIncluded) - ); + ->mapWithKeys(function ($value, $key) { + $relationResolver = is_int($key) ? new RelationResolver($value) : new RelationResolver($key, $value); + + return [$relationResolver->relationName => $relationResolver]; + })->filter(fn ($value, $key) => in_array($key, $sparseIncluded)); $resourceRelationshipKeys = $resourceRelationships->keys(); @@ -267,7 +280,7 @@ public function resolveIncludedResources(JsonApiRequest $request): array $resourceInstance = new JsonApiResource($resourceInstance->resource); } - $relationsData = $resourceInstance->withoutRequestQueryString()->resolve($request); + $relationsData = $resourceInstance->withoutRequestQueryString()->withIncludedFromLoadedRelationship()->resolve($request); $relations->push(array_filter([ 'id' => $id, diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index 59f26d849f37..38bd1fdb9331 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -206,7 +206,7 @@ public function test_it_can_resolve_relationship_with_custom_name_and_resource_c 'user_id' => $user->getKey(), ]); - $comments = Comment::factory()->create([ + $comment = Comment::factory()->create([ 'post_id' => $post1->getKey(), 'user_id' => $user->getKey(), ]); @@ -243,4 +243,63 @@ public function test_it_can_resolve_relationship_with_custom_name_and_resource_c ]) ->assertJsonMissing(['jsonapi']); } + + public function test_it_can_resolve_relationship_with_nested_relationship() + { + $now = $this->freezeSecond(); + + $user = User::factory()->create(); + + $profile = Profile::factory()->create([ + 'user_id' => $user->getKey(), + 'date_of_birth' => '2011-06-09', + 'timezone' => 'America/Chicago', + ]); + + [$post1, $post2] = Post::factory()->times(2)->create([ + 'user_id' => $user->getKey(), + ]); + + $comment = Comment::factory()->create([ + 'post_id' => $post1->getKey(), + 'user_id' => $user->getKey(), + ]); + + $this->getJson("/posts/{$post1->getKey()}?".http_build_query(['include' => 'author,comments.commenter'])) + ->assertHeader('Content-type', 'application/vnd.api+json') + ->assertExactJson([ + 'data' => [ + 'attributes' => [ + 'content' => $post1->content, + 'title' => $post1->title, + ], + 'type' => 'posts', + 'id' => (string) $post1->getKey(), + 'relationships' => [ + 'author' => [ + 'data' => [ + 'id' => (string) $user->getKey(), + 'type' => 'authors', + ], + ], + 'comments' => [ + 'data' => [ + ['id' => (string) $comment->getKey(), 'type' => 'comments'], + ], + ], + ], + ], + 'included' => [ + [ + 'attributes' => [ + 'email' => $user->email, + 'name' => $user->name, + ], + 'id' => (string) $user->getKey(), + 'type' => 'authors', + ], + ], + ]) + ->assertJsonMissing(['jsonapi']); + } } From bac37e2c0c47673d00d8eadd028324efb83630a6 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 3 Dec 2025 16:56:03 +0800 Subject: [PATCH 32/48] wip Signed-off-by: Mior Muhammad Zaki --- .../Concerns/ResolvesJsonApiElements.php | 9 ++-- .../Http/Resources/JsonApi/JsonApiRequest.php | 45 +++++++++++++++---- .../Resources/JsonApi/RelationResolver.php | 24 ++++++++-- .../Resources/JsonApi/Fixtures/Comment.php | 2 + .../JsonApi/Fixtures/PostApiResource.php | 1 + .../JsonApi/Fixtures/UserApiResource.php | 6 +++ .../Resources/JsonApi/JsonApiResourceTest.php | 15 +++++++ 7 files changed, 84 insertions(+), 18 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index af156aca515f..8fabb5f82c6f 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -185,9 +185,8 @@ protected function compileResourceRelationships(JsonApiRequest $request): void } $sparseIncluded = match (true) { - $this->usesRequestQueryString === true => $request->sparseIncluded(), - $this->usesRequestQueryString === false && $this->usesIncludedFromLoadedRelationships === true => array_keys($this->resource->getRelations()), - default => [], + $this->usesIncludedFromLoadedRelationships => array_keys($this->resource->getRelations()), + default => $request->sparseIncluded(), }; $resourceRelationships = (new Collection($this->toRelationships($request))) @@ -199,12 +198,12 @@ protected function compileResourceRelationships(JsonApiRequest $request): void $resourceRelationshipKeys = $resourceRelationships->keys(); - $this->resource->loadMissing($resourceRelationshipKeys->all()); + $this->resource->loadMissing($resourceRelationshipKeys->all() ?? []); $this->loadedRelationshipsMap = new WeakMap; $this->loadedRelationshipIdentifiers = $resourceRelationships->mapWithKeys(function (RelationResolver $relationResolver, $key) use ($request) { - $relatedModels = $relationResolver->handle($this->resource); + $relatedModels = $relationResolver->handle($this->resource)->loadMissing($request->sparseIncluded($key)); $relatedResourceClass = $relationResolver->resourceClass(); // Relationship is a collection of models... diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php index c5f0f194b1bb..f85376563bd4 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php @@ -4,30 +4,57 @@ use Illuminate\Http\Request; use Illuminate\Support\Arr; +use Illuminate\Support\Collection; class JsonApiRequest extends Request { + protected ?array $cachedSparseFields = null; + + protected ?array $cachedSparseIncluded = null; + /** * Get the request's included fields. */ public function sparseFields(string $key): array { - $fieldsets = Arr::get($this->array('fields'), $key, ''); + if (is_null($this->cachedSparseFields)) { + $this->cachedSparseFields = (new Collection($this->array('fields'))) + ->transform(fn ($fieldsets) => empty($fieldsets) ? [] : explode(',', $fieldsets)) + ->all(); + } - return empty($fieldsets) - ? [] - : explode(',', $fieldsets); + return $this->cachedSparseFields[$key] ?? []; } /** * Get the request's included relationships. */ - public function sparseIncluded(): array + public function sparseIncluded(?string $key = null): ?array { - $included = (string) $this->string('include', ''); + if (is_null($this->cachedSparseIncluded)) { + $included = (string) $this->string('include', ''); + + $this->cachedSparseIncluded = (new Collection(empty($included) ? [] : explode(',', $included))) + ->transform(function ($item) { + $with = null; + + if (str_contains($item, '.')) { + [$relation, $with] = explode('.', $item, 2); + } else { + $relation = $item; + } + + return ['relation' => $relation, 'with' => $with]; + })->mapToGroups(fn ($item) => [$item['relation'] => $item['with']]) + ->toArray(); + } + + if (is_null($key)) { + return array_keys($this->cachedSparseIncluded); + } - return empty($included) - ? [] - : explode(',', $included); + return transform($this->cachedSparseIncluded[$key] ?? null, function ($value) { + return array_filter($value); + }) ?? []; } } diff --git a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php index 47b775df6cfb..def249c0a6cf 100644 --- a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php +++ b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php @@ -12,6 +12,13 @@ */ class RelationResolver { + /** + * The relation name. + */ + public string $relationName; + + public ?string $relationWith = null; + /** * The relation resolver. * @@ -31,10 +38,15 @@ class RelationResolver * * @param \Closure(mixed):(\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model|null)|class-string<\Illuminate\Http\Resources\JsonApi\JsonApiResource>|null $resolver */ - public function __construct( - public string $relationName, - Closure|string|null $resolver = null - ) { + public function __construct(string $relationName, Closure|string|null $resolver = null) + { + if (str_contains($relationName, '.')) { + [$this->relationName, $this->relationWith] = explode('.', $relationName, 1); + } else { + $this->relationName = $relationName; + $this->relationWith = null; + } + $this->relationResolver = match (true) { $resolver instanceof Closure => $resolver, default => fn ($resource) => $resource->getRelation($this->relationName), @@ -50,6 +62,10 @@ public function __construct( */ public function handle(mixed $resource): Collection|Model|null { + if (! empty($this->relationWith)) { + $resource->loadMissing($this->relationWith); + } + return value($this->relationResolver, $resource); } diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/Comment.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/Comment.php index 05a516624d4b..aa9d4926db88 100644 --- a/tests/Integration/Http/Resources/JsonApi/Fixtures/Comment.php +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/Comment.php @@ -3,10 +3,12 @@ namespace Illuminate\Tests\Integration\Http\Resources\JsonApi\Fixtures; use Illuminate\Database\Eloquent\Attributes\UseFactory; +use Illuminate\Database\Eloquent\Attributes\UseResource; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; #[UseFactory(CommentFactory::class)] +#[UseResource(CommentApiResource::class)] class Comment extends Model { use HasFactory; diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/PostApiResource.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/PostApiResource.php index dd847d304c5b..874b703577a3 100644 --- a/tests/Integration/Http/Resources/JsonApi/Fixtures/PostApiResource.php +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/PostApiResource.php @@ -13,5 +13,6 @@ class PostApiResource extends JsonApiResource protected array $relationships = [ 'author' => AuthorApiResource::class, + 'comments', ]; } diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/UserApiResource.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/UserApiResource.php index bce50ff6da07..6fe76f81db1d 100644 --- a/tests/Integration/Http/Resources/JsonApi/Fixtures/UserApiResource.php +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/UserApiResource.php @@ -22,4 +22,10 @@ public function toAttributes(Request $request) 'email' => $this->email, ]; } + + #[\Override] + public function toType(Request $request) + { + return 'users'; + } } diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index 38bd1fdb9331..e020455e6c19 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -298,6 +298,21 @@ public function test_it_can_resolve_relationship_with_nested_relationship() 'id' => (string) $user->getKey(), 'type' => 'authors', ], + [ + 'attributes' => [ + 'content' => $comment->content, + ], + 'id' => (string) $comment->getKey(), + 'type' => 'comments', + 'relationships' => [ + 'commenter' => [ + 'data' => [ + 'id' => (string) $user->getKey(), + 'type' => 'users', + ], + ], + ], + ], ], ]) ->assertJsonMissing(['jsonapi']); From 29f6f53d56a999e3523eabead90983dada75bba2 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 3 Dec 2025 08:56:29 +0000 Subject: [PATCH 33/48] Apply fixes from StyleCI --- src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php index f85376563bd4..e2c29840b8c0 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php @@ -3,7 +3,6 @@ namespace Illuminate\Http\Resources\JsonApi; use Illuminate\Http\Request; -use Illuminate\Support\Arr; use Illuminate\Support\Collection; class JsonApiRequest extends Request From d9d9d97c18d60c58ad2b5494a0ae88e659623add Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 3 Dec 2025 17:36:27 +0800 Subject: [PATCH 34/48] wip Signed-off-by: Mior Muhammad Zaki --- .../Resources/JsonApi/Concerns/ResolvesJsonApiElements.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 8fabb5f82c6f..d8bdfcaefad9 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -203,9 +203,13 @@ protected function compileResourceRelationships(JsonApiRequest $request): void $this->loadedRelationshipsMap = new WeakMap; $this->loadedRelationshipIdentifiers = $resourceRelationships->mapWithKeys(function (RelationResolver $relationResolver, $key) use ($request) { - $relatedModels = $relationResolver->handle($this->resource)->loadMissing($request->sparseIncluded($key)); + $relatedModels = $relationResolver->handle($this->resource); $relatedResourceClass = $relationResolver->resourceClass(); + if (! is_null($relatedModels)) { + $relatedModels->loadMissing($request->sparseIncluded($key)); + } + // Relationship is a collection of models... if ($relatedModels instanceof Collection) { $relatedModels = $relatedModels->values(); From 4eef51c38e9134e102aab9d43930408eefeb348f Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 3 Dec 2025 17:37:12 +0800 Subject: [PATCH 35/48] wip Signed-off-by: Mior Muhammad Zaki --- src/Illuminate/Http/Resources/JsonApi/RelationResolver.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php index def249c0a6cf..9873c7f5e9e1 100644 --- a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php +++ b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php @@ -41,7 +41,7 @@ class RelationResolver public function __construct(string $relationName, Closure|string|null $resolver = null) { if (str_contains($relationName, '.')) { - [$this->relationName, $this->relationWith] = explode('.', $relationName, 1); + [$this->relationName, $this->relationWith] = explode('.', $relationName, 2); } else { $this->relationName = $relationName; $this->relationWith = null; @@ -62,10 +62,6 @@ public function __construct(string $relationName, Closure|string|null $resolver */ public function handle(mixed $resource): Collection|Model|null { - if (! empty($this->relationWith)) { - $resource->loadMissing($this->relationWith); - } - return value($this->relationResolver, $resource); } From 17faaff2eb82cf920a92acd2a5e1473504af9728 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 3 Dec 2025 17:40:12 +0800 Subject: [PATCH 36/48] wip Signed-off-by: Mior Muhammad Zaki --- .../Http/Resources/JsonApi/RelationResolver.php | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php index 9873c7f5e9e1..94b97dd41814 100644 --- a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php +++ b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php @@ -12,13 +12,6 @@ */ class RelationResolver { - /** - * The relation name. - */ - public string $relationName; - - public ?string $relationWith = null; - /** * The relation resolver. * @@ -38,15 +31,8 @@ class RelationResolver * * @param \Closure(mixed):(\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model|null)|class-string<\Illuminate\Http\Resources\JsonApi\JsonApiResource>|null $resolver */ - public function __construct(string $relationName, Closure|string|null $resolver = null) + public function __construct(public string $relationName, Closure|string|null $resolver = null) { - if (str_contains($relationName, '.')) { - [$this->relationName, $this->relationWith] = explode('.', $relationName, 2); - } else { - $this->relationName = $relationName; - $this->relationWith = null; - } - $this->relationResolver = match (true) { $resolver instanceof Closure => $resolver, default => fn ($resource) => $resource->getRelation($this->relationName), From 29a4a5d6c3d6a1f4432c41e52dc8cedbbf18b4e9 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 4 Dec 2025 07:22:54 +0800 Subject: [PATCH 37/48] wip Signed-off-by: Mior Muhammad Zaki --- .../Http/Resources/Json/JsonResource.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Illuminate/Http/Resources/Json/JsonResource.php b/src/Illuminate/Http/Resources/Json/JsonResource.php index 33c02cbc7b14..545b16d9cac5 100644 --- a/src/Illuminate/Http/Resources/Json/JsonResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonResource.php @@ -132,7 +132,7 @@ public function resolve($request = null) */ public function toAttributes(Request $request) { - return $this->resolveResourceData($request); + return $this->toArray($request); } /** @@ -143,7 +143,13 @@ public function toAttributes(Request $request) */ public function resolveResourceData(Request $request) { - return $this->toArray($request); + if (is_null($this->resource)) { + return []; + } + + return is_array($this->resource) + ? $this->resource + : $this->resource->toArray(); } /** @@ -154,13 +160,7 @@ public function resolveResourceData(Request $request) */ public function toArray(Request $request) { - if (is_null($this->resource)) { - return []; - } - - return is_array($this->resource) - ? $this->resource - : $this->resource->toArray(); + return $this->resolveResourceData($request); } /** From b400eecb26c34e84acca3277960127015c8e145e Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 4 Dec 2025 07:25:53 +0800 Subject: [PATCH 38/48] wip Signed-off-by: Mior Muhammad Zaki --- .../Http/Resources/Json/JsonResource.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Illuminate/Http/Resources/Json/JsonResource.php b/src/Illuminate/Http/Resources/Json/JsonResource.php index 545b16d9cac5..ebef5e7aab8b 100644 --- a/src/Illuminate/Http/Resources/Json/JsonResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonResource.php @@ -111,7 +111,7 @@ protected static function newCollection($resource) */ public function resolve($request = null) { - $data = $this->toAttributes( + $data = $this->resolveResourceData( $request ?: $this->resolveRequestFromContainer() ); @@ -143,13 +143,7 @@ public function toAttributes(Request $request) */ public function resolveResourceData(Request $request) { - if (is_null($this->resource)) { - return []; - } - - return is_array($this->resource) - ? $this->resource - : $this->resource->toArray(); + return $this->toAttributes($request); } /** @@ -160,7 +154,13 @@ public function resolveResourceData(Request $request) */ public function toArray(Request $request) { - return $this->resolveResourceData($request); + if (is_null($this->resource)) { + return []; + } + + return is_array($this->resource) + ? $this->resource + : $this->resource->toArray(); } /** From 17a7b7ff1358552b59bfc652d870ac328704b1d9 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 4 Dec 2025 07:36:18 +0800 Subject: [PATCH 39/48] wip Signed-off-by: Mior Muhammad Zaki --- src/Illuminate/Http/Resources/Json/JsonResource.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Illuminate/Http/Resources/Json/JsonResource.php b/src/Illuminate/Http/Resources/Json/JsonResource.php index ebef5e7aab8b..7590a95d8eb0 100644 --- a/src/Illuminate/Http/Resources/Json/JsonResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonResource.php @@ -132,6 +132,10 @@ public function resolve($request = null) */ public function toAttributes(Request $request) { + if (property_exists($this, 'attributes')) { + return $this->attributes; + } + return $this->toArray($request); } From e76de92961958c46ef40934dbd2334a7584ed4dd Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 4 Dec 2025 07:43:07 +0800 Subject: [PATCH 40/48] wip Signed-off-by: Mior Muhammad Zaki --- .../JsonApi/Concerns/ResolvesJsonApiElements.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index d8bdfcaefad9..97540e5dd2e2 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -338,8 +338,16 @@ public static function resourceTypeFromModel(Model $model): string $morphMap = Relation::getMorphAlias($modelClassName); - return Str::of( + return static::normalizeResourceType( $morphMap !== $modelClassName ? $morphMap : class_basename($modelClassName) - )->snake()->pluralStudly(); + ); + } + + /** + * Normalize the resource type. + */ + public static function normalizeResourceType(string $value): string + { + return Str::of($value)->snake()->pluralStudly(); } } From 48042f35c9e66b84478b4a64768284ef6e155a68 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 4 Dec 2025 07:45:56 +0800 Subject: [PATCH 41/48] wip Signed-off-by: Mior Muhammad Zaki --- src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php index e2c29840b8c0..c869f1382578 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php @@ -7,8 +7,14 @@ class JsonApiRequest extends Request { + /** + * Cached sparse fieldset. + */ protected ?array $cachedSparseFields = null; + /** + * Cached sparse included. + */ protected ?array $cachedSparseIncluded = null; /** From 3ffb5c57d4cdc45eb705a55386b75ec3a7db1ca5 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 4 Dec 2025 10:52:01 +0800 Subject: [PATCH 42/48] wip Signed-off-by: Mior Muhammad Zaki --- .../Concerns/ResolvesJsonApiElements.php | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 97540e5dd2e2..df758426f99c 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -44,6 +44,11 @@ trait ResolvesJsonApiElements */ protected array $loadedRelationshipIdentifiers = []; + /** + * Determine relationships should rely on request's query string. + * + * @return $this + */ public function withRequestQueryString(bool $value = true) { $this->usesRequestQueryString = $value; @@ -51,12 +56,22 @@ public function withRequestQueryString(bool $value = true) return $this; } + /** + * Determine relationships should not be relied on request's query string. + * + * @return $this + */ public function withoutRequestQueryString() { return $this->withRequestQueryString(false); } - public function withIncludedFromLoadedRelationship() + /** + * Determine relationship should include loaded relationships. + * + * @return $this + */ + public function withIncludedFromLoadedRelationships() { $this->usesIncludedFromLoadedRelationships = true; @@ -283,7 +298,7 @@ public function resolveIncludedResources(JsonApiRequest $request): array $resourceInstance = new JsonApiResource($resourceInstance->resource); } - $relationsData = $resourceInstance->withoutRequestQueryString()->withIncludedFromLoadedRelationship()->resolve($request); + $relationsData = $resourceInstance->withoutRequestQueryString()->withIncludedFromLoadedRelationships()->resolve($request); $relations->push(array_filter([ 'id' => $id, From ad152087476deaee4e63f47e4909c613124f0f08 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 4 Dec 2025 11:11:28 +0800 Subject: [PATCH 43/48] wip Signed-off-by: Mior Muhammad Zaki --- .../Integration/Http/Resources/JsonApi/TestCase.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/Integration/Http/Resources/JsonApi/TestCase.php b/tests/Integration/Http/Resources/JsonApi/TestCase.php index 4904345733cc..12afc785818e 100644 --- a/tests/Integration/Http/Resources/JsonApi/TestCase.php +++ b/tests/Integration/Http/Resources/JsonApi/TestCase.php @@ -2,9 +2,10 @@ namespace Illuminate\Tests\Integration\Http\Resources\JsonApi; +use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Testing\LazilyRefreshDatabase; -use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\JsonApi\JsonApiResource; +use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Tests\Integration\Http\Resources\JsonApi\Fixtures\Post; use Illuminate\Tests\Integration\Http\Resources\JsonApi\Fixtures\User; use Orchestra\Testbench\Attributes\WithConfig; @@ -16,6 +17,15 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase { use LazilyRefreshDatabase; + /** {@inheritdoc} */ + #[\Override] + protected function setUp(): void + { + Model::shouldBeStrict(true); + + parent::setUp(); + } + /** {@inheritdoc} */ #[\Override] protected function tearDown(): void From 7f75fb0d8bb87df48ef3611239d0e24ea82ed442 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 4 Dec 2025 03:11:51 +0000 Subject: [PATCH 44/48] Apply fixes from StyleCI --- tests/Integration/Http/Resources/JsonApi/TestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integration/Http/Resources/JsonApi/TestCase.php b/tests/Integration/Http/Resources/JsonApi/TestCase.php index 12afc785818e..aa87168d426a 100644 --- a/tests/Integration/Http/Resources/JsonApi/TestCase.php +++ b/tests/Integration/Http/Resources/JsonApi/TestCase.php @@ -4,8 +4,8 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Testing\LazilyRefreshDatabase; -use Illuminate\Http\Resources\JsonApi\JsonApiResource; use Illuminate\Http\Resources\Json\JsonResource; +use Illuminate\Http\Resources\JsonApi\JsonApiResource; use Illuminate\Tests\Integration\Http\Resources\JsonApi\Fixtures\Post; use Illuminate\Tests\Integration\Http\Resources\JsonApi\Fixtures\User; use Orchestra\Testbench\Attributes\WithConfig; From 2ec397252c536d4ad79136e3202967274434f124 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 4 Dec 2025 21:29:53 +0800 Subject: [PATCH 45/48] [JSON:API] Handles Nested Relationship (#58009) * wip Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * Update src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php --------- Signed-off-by: Mior Muhammad Zaki Co-authored-by: StyleCI Bot --- .../Concerns/ResolvesJsonApiElements.php | 165 ++++++++++++------ .../Resources/JsonApi/JsonApiResourceTest.php | 9 + 2 files changed, 119 insertions(+), 55 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index df758426f99c..e0efca7271b7 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -2,6 +2,7 @@ namespace Illuminate\Http\Resources\JsonApi\Concerns; +use Generator; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\AsPivot; @@ -16,9 +17,9 @@ use Illuminate\Http\Resources\MissingValue; use Illuminate\Support\Arr; use Illuminate\Support\Collection; +use Illuminate\Support\LazyCollection; use Illuminate\Support\Str; use JsonSerializable; -use WeakMap; trait ResolvesJsonApiElements { @@ -35,9 +36,9 @@ trait ResolvesJsonApiElements /** * Cached loaded relationships map. * - * @var \WeakMap|null + * @var arraytoId($request))) { return $resourceId; @@ -123,7 +124,7 @@ protected function resolveResourceIdentifier(JsonApiRequest $request): string * * @throws ResourceIdentificationException */ - protected function resolveResourceType(JsonApiRequest $request): string + public function resolveResourceType(JsonApiRequest $request): string { if (! is_null($resourceType = $this->toType($request))) { return $resourceType; @@ -195,7 +196,7 @@ protected function resolveResourceRelationshipIdentifiers(JsonApiRequest $reques */ protected function compileResourceRelationships(JsonApiRequest $request): void { - if ($this->loadedRelationshipsMap instanceof WeakMap) { + if (! is_null($this->loadedRelationshipsMap)) { return; } @@ -205,74 +206,124 @@ protected function compileResourceRelationships(JsonApiRequest $request): void }; $resourceRelationships = (new Collection($this->toRelationships($request))) - ->mapWithKeys(function ($value, $key) { - $relationResolver = is_int($key) ? new RelationResolver($value) : new RelationResolver($key, $value); - - return [$relationResolver->relationName => $relationResolver]; - })->filter(fn ($value, $key) => in_array($key, $sparseIncluded)); + ->transform(fn ($value, $key) => is_int($key) ? new RelationResolver($value) : new RelationResolver($key, $value)) + ->mapWithKeys(fn ($relationResolver) => [$relationResolver->relationName => $relationResolver]) + ->filter(fn ($value, $key) => in_array($key, $sparseIncluded)); $resourceRelationshipKeys = $resourceRelationships->keys(); $this->resource->loadMissing($resourceRelationshipKeys->all() ?? []); - $this->loadedRelationshipsMap = new WeakMap; + $this->loadedRelationshipsMap = []; - $this->loadedRelationshipIdentifiers = $resourceRelationships->mapWithKeys(function (RelationResolver $relationResolver, $key) use ($request) { - $relatedModels = $relationResolver->handle($this->resource); - $relatedResourceClass = $relationResolver->resourceClass(); + $this->loadedRelationshipIdentifiers = (new LazyCollection(function () use ($request, $resourceRelationships) { + foreach ($resourceRelationships as $relationName => $relationResolver) { + $relatedModels = $relationResolver->handle($this->resource); + $relatedResourceClass = $relationResolver->resourceClass(); - if (! is_null($relatedModels)) { - $relatedModels->loadMissing($request->sparseIncluded($key)); - } + if (! is_null($relatedModels)) { + $relatedModels->loadMissing($request->sparseIncluded($relationName)); + } - // Relationship is a collection of models... - if ($relatedModels instanceof Collection) { - $relatedModels = $relatedModels->values(); + yield from $this->compileResourceRelationshipUsingResolver( + $this->resource, + $relationResolver, + $relatedModels, + $request + ); + } + }))->all(); + } - if ($relatedModels->isEmpty()) { - return [$key => ['data' => $relatedModels]]; - } + /** + * Compile resource relations. + */ + protected function compileResourceRelationshipUsingResolver( + mixed $resource, + RelationResolver $relationResolver, + Collection|Model|null $relatedModels, + JsonApiRequest $request + ): Generator { + $relationName = $relationResolver->relationName; + $resourceClass = $relationResolver->resourceClass(); + + // Relationship is a collection of models... + if ($relatedModels instanceof Collection) { + $relatedModels = $relatedModels->values(); + + if ($relatedModels->isEmpty()) { + yield $relationName => ['data' => $relatedModels]; + + return; + } - $relationship = $this->resource->{$key}(); + $relationship = $resource->{$relationName}(); + $isUnique = ! $relationship instanceof BelongsToMany; - $isUnique = ! $relationship instanceof BelongsToMany; + yield $relationName => ['data' => $relatedModels->map(function ($relatedModel) use ($request, $resourceClass, $isUnique) { + $relatedResource = rescue(fn () => $relatedModel->toResource($resourceClass), new JsonApiResource($relatedModel)); - $key = $relationResolver->resourceType($relatedModels, $request); + return transform( + [$relatedResource->resolveResourceType($request), $relatedResource->resolveResourceIdentifier($request)], + function ($uniqueKey) use ($request, $relatedModel, $relatedResource, $isUnique) { + $this->loadedRelationshipsMap[] = [$relatedResource, ...$uniqueKey, $isUnique]; - return [$key => ['data' => $relatedModels->map(function ($relation) use ($key, $relatedResourceClass, $isUnique) { - return transform([$key, static::resourceIdFromModel($relation)], function ($uniqueKey) use ($relation, $relatedResourceClass, $isUnique) { - $this->loadedRelationshipsMap[$relation] = [...$uniqueKey, $relatedResourceClass, $isUnique]; + $this->compileIncludedNestedRelationshipsMap($relatedModel, $relatedResource, $request); return [ 'id' => $uniqueKey[1], 'type' => $uniqueKey[0], ]; - }); - })]]; - } + } + ); + })->all()]; - // Relationship is a single model... - $relatedModel = $relatedModels; + return; + } - if (is_null($relatedModel)) { - return [$key => null]; - } elseif ($relatedModel instanceof Pivot || - in_array(AsPivot::class, class_uses_recursive($relatedModel), true)) { - return [$key => new MissingValue]; - } + // Relationship is a single model... + $relatedModel = $relatedModels; - return [$key => ['data' => transform( - [$relationResolver->resourceType($relatedModel, $request), static::resourceIdFromModel($relatedModel)], - function ($uniqueKey) use ($relatedModel, $relatedResourceClass) { - $this->loadedRelationshipsMap[$relatedModel] = [...$uniqueKey, $relatedResourceClass, true]; + if (is_null($relatedModel)) { + yield $relationName => null; - return [ - 'id' => $uniqueKey[1], - 'type' => $uniqueKey[0], - ]; - } - )]]; - })->all(); + return; + } elseif ($relatedModel instanceof Pivot || + in_array(AsPivot::class, class_uses_recursive($relatedModel), true)) { + yield $relationName => new MissingValue; + + return; + } + + $relatedResource = rescue(fn () => $relatedModel->toResource($resourceClass), new JsonApiResource($relatedModel)); + + yield $relationName => ['data' => transform( + [$relatedResource->resolveResourceType($request), $relatedResource->resolveResourceIdentifier($request)], + function ($uniqueKey) use ($relatedModel, $relatedResource, $request) { + $this->loadedRelationshipsMap[] = [$relatedResource, ...$uniqueKey, true]; + + $this->compileIncludedNestedRelationshipsMap($relatedModel, $relatedResource, $request); + + return [ + 'id' => $uniqueKey[1], + 'type' => $uniqueKey[0], + ]; + } + )]; + } + + /** + * Compile included relationships map. + */ + protected function compileIncludedNestedRelationshipsMap(Model $relation, JsonApiResource $resource, JsonApiRequest $request): void + { + (new Collection($resource->toRelationships($request))) + ->transform(fn ($value, $key) => is_int($key) ? new RelationResolver($value) : new RelationResolver($key, $value)) + ->mapWithKeys(fn ($relationResolver) => [$relationResolver->relationName => $relationResolver]) + ->filter(fn ($value, $key) => in_array($key, array_keys($relation->getRelations()))) + ->each(function ($relationResolver, $key) use ($relation, $request) { + $this->compileResourceRelationshipUsingResolver($relation, $relationResolver, $relation->getRelation($key), $request); + }); } /** @@ -288,10 +339,10 @@ public function resolveIncludedResources(JsonApiRequest $request): array $relations = new Collection; - foreach ($this->loadedRelationshipsMap as $relation => $value) { - [$type, $id, $relatedResourceClass, $isUnique] = $value; + $index = 0; - $resourceInstance = rescue(fn () => $relation->toResource($relatedResourceClass), new JsonApiResource($relation), false); + while ($index < count($this->loadedRelationshipsMap)) { + [$resourceInstance, $type, $id, $isUnique] = $this->loadedRelationshipsMap[$index]; if (! $resourceInstance instanceof JsonApiResource && $resourceInstance instanceof JsonResource) { @@ -300,6 +351,8 @@ public function resolveIncludedResources(JsonApiRequest $request): array $relationsData = $resourceInstance->withoutRequestQueryString()->withIncludedFromLoadedRelationships()->resolve($request); + array_push($this->loadedRelationshipsMap, ...$resourceInstance->loadedRelationshipsMap); + $relations->push(array_filter([ 'id' => $id, 'type' => $type, @@ -309,6 +362,8 @@ public function resolveIncludedResources(JsonApiRequest $request): array 'links' => Arr::get($relationsData, 'data.links'), 'meta' => Arr::get($relationsData, 'data.meta'), ])); + + $index++; } return $relations->uniqueStrict(fn ($relation) => $relation['_uniqueKey']) diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index e020455e6c19..ced43913ead6 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -267,6 +267,7 @@ public function test_it_can_resolve_relationship_with_nested_relationship() $this->getJson("/posts/{$post1->getKey()}?".http_build_query(['include' => 'author,comments.commenter'])) ->assertHeader('Content-type', 'application/vnd.api+json') + ->dump() ->assertExactJson([ 'data' => [ 'attributes' => [ @@ -313,6 +314,14 @@ public function test_it_can_resolve_relationship_with_nested_relationship() ], ], ], + [ + 'attributes' => [ + 'email' => $user->email, + 'name' => $user->name, + ], + 'id' => (string) $user->getKey(), + 'type' => 'users', + ], ], ]) ->assertJsonMissing(['jsonapi']); From 5b3eef7b7538f275e2dae349088fff167e1d3c3a Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Fri, 5 Dec 2025 10:18:43 +0800 Subject: [PATCH 46/48] Remove dump from JsonApiResourceTest Removed debug dump from JSON API resource test. --- tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index ced43913ead6..82909d11ffdc 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -267,7 +267,6 @@ public function test_it_can_resolve_relationship_with_nested_relationship() $this->getJson("/posts/{$post1->getKey()}?".http_build_query(['include' => 'author,comments.commenter'])) ->assertHeader('Content-type', 'application/vnd.api+json') - ->dump() ->assertExactJson([ 'data' => [ 'attributes' => [ From 4a0c6cd79ba655f0db0fa5e3b74a42894973f0ce Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 9 Dec 2025 13:07:27 -0600 Subject: [PATCH 47/48] formatting --- .../Concerns/ResolvesJsonApiElements.php | 92 ++++++++++--------- .../Resources/JsonApi/RelationResolver.php | 26 +----- 2 files changed, 49 insertions(+), 69 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index e0efca7271b7..e199c64ca680 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -24,14 +24,14 @@ trait ResolvesJsonApiElements { /** - * Determine whether resource use included and fields from the request object. + * Determine whether resources respect inclusions and fields from the request. */ protected bool $usesRequestQueryString = true; /** * Determine whether included relationship for the resource from eager loaded relationship. */ - protected bool $usesIncludedFromLoadedRelationships = false; + protected bool $includesPreviouslyLoadedRelationships = false; /** * Cached loaded relationships map. @@ -45,40 +45,6 @@ trait ResolvesJsonApiElements */ protected array $loadedRelationshipIdentifiers = []; - /** - * Determine relationships should rely on request's query string. - * - * @return $this - */ - public function withRequestQueryString(bool $value = true) - { - $this->usesRequestQueryString = $value; - - return $this; - } - - /** - * Determine relationships should not be relied on request's query string. - * - * @return $this - */ - public function withoutRequestQueryString() - { - return $this->withRequestQueryString(false); - } - - /** - * Determine relationship should include loaded relationships. - * - * @return $this - */ - public function withIncludedFromLoadedRelationships() - { - $this->usesIncludedFromLoadedRelationships = true; - - return $this; - } - /** * Resolves `data` for the resource. */ @@ -201,7 +167,7 @@ protected function compileResourceRelationships(JsonApiRequest $request): void } $sparseIncluded = match (true) { - $this->usesIncludedFromLoadedRelationships => array_keys($this->resource->getRelations()), + $this->includesPreviouslyLoadedRelationships => array_keys($this->resource->getRelations()), default => $request->sparseIncluded(), }; @@ -226,10 +192,10 @@ protected function compileResourceRelationships(JsonApiRequest $request): void } yield from $this->compileResourceRelationshipUsingResolver( + $request, $this->resource, $relationResolver, $relatedModels, - $request ); } }))->all(); @@ -240,9 +206,9 @@ protected function compileResourceRelationships(JsonApiRequest $request): void */ protected function compileResourceRelationshipUsingResolver( mixed $resource, + JsonApiRequest $request, RelationResolver $relationResolver, - Collection|Model|null $relatedModels, - JsonApiRequest $request + Collection|Model|null $relatedModels ): Generator { $relationName = $relationResolver->relationName; $resourceClass = $relationResolver->resourceClass(); @@ -258,6 +224,7 @@ protected function compileResourceRelationshipUsingResolver( } $relationship = $resource->{$relationName}(); + $isUnique = ! $relationship instanceof BelongsToMany; yield $relationName => ['data' => $relatedModels->map(function ($relatedModel) use ($request, $resourceClass, $isUnique) { @@ -268,7 +235,7 @@ protected function compileResourceRelationshipUsingResolver( function ($uniqueKey) use ($request, $relatedModel, $relatedResource, $isUnique) { $this->loadedRelationshipsMap[] = [$relatedResource, ...$uniqueKey, $isUnique]; - $this->compileIncludedNestedRelationshipsMap($relatedModel, $relatedResource, $request); + $this->compileIncludedNestedRelationshipsMap($request, $relatedModel, $relatedResource); return [ 'id' => $uniqueKey[1], @@ -302,7 +269,7 @@ function ($uniqueKey) use ($request, $relatedModel, $relatedResource, $isUnique) function ($uniqueKey) use ($relatedModel, $relatedResource, $request) { $this->loadedRelationshipsMap[] = [$relatedResource, ...$uniqueKey, true]; - $this->compileIncludedNestedRelationshipsMap($relatedModel, $relatedResource, $request); + $this->compileIncludedNestedRelationshipsMap($request, $relatedModel, $relatedResource); return [ 'id' => $uniqueKey[1], @@ -315,7 +282,7 @@ function ($uniqueKey) use ($relatedModel, $relatedResource, $request) { /** * Compile included relationships map. */ - protected function compileIncludedNestedRelationshipsMap(Model $relation, JsonApiResource $resource, JsonApiRequest $request): void + protected function compileIncludedNestedRelationshipsMap(JsonApiRequest $request, Model $relation, JsonApiResource $resource): void { (new Collection($resource->toRelationships($request))) ->transform(fn ($value, $key) => is_int($key) ? new RelationResolver($value) : new RelationResolver($key, $value)) @@ -349,7 +316,10 @@ public function resolveIncludedResources(JsonApiRequest $request): array $resourceInstance = new JsonApiResource($resourceInstance->resource); } - $relationsData = $resourceInstance->withoutRequestQueryString()->withIncludedFromLoadedRelationships()->resolve($request); + $relationsData = $resourceInstance + ->withoutRequestQueryString() + ->includePreviouslyLoadedRelationships() + ->resolve($request); array_push($this->loadedRelationshipsMap, ...$resourceInstance->loadedRelationshipsMap); @@ -413,6 +383,40 @@ public static function resourceTypeFromModel(Model $model): string ); } + /** + * Indicate that relationship loading should respect the request's "includes" query string. + * + * @return $this + */ + public function withRequestQueryString(bool $value = true) + { + $this->usesRequestQueryString = $value; + + return $this; + } + + /** + * Indicate that relationship loading should not rely on the request's "includes" query string. + * + * @return $this + */ + public function withoutRequestQueryString() + { + return $this->withRequestQueryString(false); + } + + /** + * Determine relationship should include loaded relationships. + * + * @return $this + */ + public function includePreviouslyLoadedRelationships() + { + $this->includesPreviouslyLoadedRelationships = true; + + return $this; + } + /** * Normalize the resource type. */ diff --git a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php index 94b97dd41814..772acff826a9 100644 --- a/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php +++ b/src/Illuminate/Http/Resources/JsonApi/RelationResolver.php @@ -5,7 +5,6 @@ use Closure; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; -use Illuminate\Http\Resources\JsonApi\Exceptions\ResourceIdentificationException; /** * @internal @@ -44,7 +43,7 @@ public function __construct(public string $relationName, Closure|string|null $re } /** - * Resolve relation for a resource. + * Resolve the relation for a resource. */ public function handle(mixed $resource): Collection|Model|null { @@ -60,27 +59,4 @@ public function resourceClass(): ?string { return $this->relationResourceClass; } - - /** - * Get the relation resource type. - * - * @throws \Illuminate\Http\Resources\JsonApi\Exceptions\ResourceIdentificationException - */ - public function resourceType(Collection|Model $resources, JsonApiRequest $request): ?string - { - $resource = $resources instanceof Collection ? $resources->first() : $resources; - - if (is_null($resourceClass = $this->resourceClass())) { - return JsonApiResource::resourceTypeFromModel($resource); - } - - $relatedResource = new $resourceClass($resource); - - return tap($relatedResource->toType($request), function ($resourceType) use ($relatedResource) { - throw_if( - is_null($resourceType), - ResourceIdentificationException::attemptingToDetermineTypeFor($relatedResource) - ); - }); - } } From 4e318f3f90eb1a325cae32dac5cdf9d45b94c3a1 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 9 Dec 2025 13:08:52 -0600 Subject: [PATCH 48/48] formatting --- .../Resources/JsonApi/Concerns/ResolvesJsonApiElements.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index e199c64ca680..1ea9188f7792 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -205,8 +205,8 @@ protected function compileResourceRelationships(JsonApiRequest $request): void * Compile resource relations. */ protected function compileResourceRelationshipUsingResolver( - mixed $resource, JsonApiRequest $request, + mixed $resource, RelationResolver $relationResolver, Collection|Model|null $relatedModels ): Generator { @@ -289,7 +289,7 @@ protected function compileIncludedNestedRelationshipsMap(JsonApiRequest $request ->mapWithKeys(fn ($relationResolver) => [$relationResolver->relationName => $relationResolver]) ->filter(fn ($value, $key) => in_array($key, array_keys($relation->getRelations()))) ->each(function ($relationResolver, $key) use ($relation, $request) { - $this->compileResourceRelationshipUsingResolver($relation, $relationResolver, $relation->getRelation($key), $request); + $this->compileResourceRelationshipUsingResolver($request, $relation, $relationResolver, $relation->getRelation($key)); }); }