diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 4209cd96ae3a..5cb26268313f 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -45,6 +45,19 @@ trait ResolvesJsonApiElements */ protected array $loadedRelationshipIdentifiers = []; + /** + * The maximum relationship depth. + */ + public static int $maxRelationshipDepth = 5; + + /** + * Specify the maximum relationship depth. + */ + public static function maxRelationshipDepth(int $depth): void + { + static::$maxRelationshipDepth = max(0, $depth); + } + /** * Resolves `data` for the resource. */ @@ -197,7 +210,9 @@ protected function compileResourceRelationships(JsonApiRequest $request): void $relatedResourceClass = $relationResolver->resourceClass(); if (! is_null($relatedModels) && $this->includesPreviouslyLoadedRelationships === false) { - $relatedModels->loadMissing($request->sparseIncluded($relationName)); + if (! empty($relations = $request->sparseIncluded($relationName))) { + $relatedModels->loadMissing($relations); + } } yield from $this->compileResourceRelationshipUsingResolver( diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php index c869f1382578..075f52ce312b 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php @@ -3,6 +3,7 @@ namespace Illuminate\Http\Resources\JsonApi; use Illuminate\Http\Request; +use Illuminate\Support\Arr; use Illuminate\Support\Collection; class JsonApiRequest extends Request @@ -59,7 +60,12 @@ public function sparseIncluded(?string $key = null): ?array } return transform($this->cachedSparseIncluded[$key] ?? null, function ($value) { - return array_filter($value); + return (new Collection(Arr::wrap($value))) + ->transform(function ($item) { + $item = implode('.', Arr::take(explode('.', $item), JsonApiResource::$maxRelationshipDepth - 1)); + + return ! empty($item) ? $item : null; + })->filter()->all(); }) ?? []; } } diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index fa00c3e93e14..06100158d7f1 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -250,5 +250,6 @@ public static function flushState() parent::flushState(); static::$jsonApiInformation = []; + static::$maxRelationshipDepth = 3; } } diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiRequestTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiRequestTest.php index 38f02fa07219..ff96ec1f7879 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiRequestTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiRequestTest.php @@ -3,6 +3,7 @@ namespace Illuminate\Tests\Integration\Http\Resources\JsonApi; use Illuminate\Http\Resources\JsonApi\JsonApiRequest; +use Illuminate\Http\Resources\JsonApi\JsonApiResource; class JsonApiRequestTest extends TestCase { @@ -41,6 +42,20 @@ public function testItCanResolveSparseIncluded() $this->assertSame(['user.profile'], $request->sparseIncluded('profile')); } + public function testItCanREsolveSparseIncludedWithMaxRelationshipNesting() + { + JsonApiResource::maxRelationshipDepth(2); + + $request = JsonApiRequest::create(uri: '/?'.http_build_query([ + 'include' => 'teams,posts.author,posts.comments,profile.user.profile', + ])); + + $this->assertSame(['teams', 'posts', 'profile'], $request->sparseIncluded()); + $this->assertSame([], $request->sparseIncluded('teams')); + $this->assertSame(['author', 'comments'], $request->sparseIncluded('posts')); + $this->assertSame(['user'], $request->sparseIncluded('profile')); + } + public function testItCanResolveEmptySparseIncluded() { $request = JsonApiRequest::create(uri: '/'); diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index fca806565c39..1d73038fcbfd 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\Http\Resources\JsonApi\JsonApiResource; 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; @@ -465,6 +466,63 @@ public function testItCanResolveRelationshipWithRecursiveNestedRelationship() ->assertJsonMissing(['jsonapi']); } + public function testItCanResolveRelationshipWithRecursiveNestedRelationshipLimitedToDepthConfiguration() + { + JsonApiResource::maxRelationshipDepth(2); + + $now = $this->freezeSecond(); + + $user = User::factory()->create(); + + $profile = Profile::factory()->create([ + 'user_id' => $user->getKey(), + 'date_of_birth' => '2011-06-09', + 'timezone' => 'America/Chicago', + ]); + + $this->getJson("/users/{$user->getKey()}?".http_build_query(['include' => 'profile.user.profile'])) + ->assertHeader('Content-type', 'application/vnd.api+json') + ->assertExactJson([ + 'data' => [ + 'attributes' => [ + 'email' => $user->email, + 'name' => $user->name, + ], + 'id' => (string) $user->getKey(), + 'type' => 'users', + 'relationships' => [ + 'profile' => [ + 'data' => ['id' => (string) $profile->getKey(), 'type' => 'profiles'], + ], + ], + ], + 'included' => [ + [ + 'attributes' => [ + 'date_of_birth' => '2011-06-09', + 'timezone' => 'America/Chicago', + ], + 'id' => (string) $profile->getKey(), + 'type' => 'profiles', + 'relationships' => [ + 'user' => [ + 'data' => ['id' => (string) $user->getKey(), 'type' => 'users'], + ], + ], + ], + [ + 'attributes' => [ + 'email' => $user->email, + 'name' => $user->name, + ], + 'id' => (string) $user->getKey(), + 'type' => 'users', + ], + ], + ]) + ->assertJsonMissing(['jsonapi']); + } + public function testItCanResolveRelationshipWithoutRedundantIncludedRelationship() { $now = $this->freezeSecond();