From c69a2e57cbd262b05ac19a049eca0ce2c9f4e6f4 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Mon, 3 Nov 2025 20:04:11 +0800 Subject: [PATCH 01/34] [JSON:API] Supports `JsonApiResource::toRelationships()` Signed-off-by: Mior Muhammad Zaki --- .../Concerns/ResolvesJsonApiElements.php | 20 ++- .../Resources/JsonApi/JsonApiResource.php | 9 ++ .../Resources/JsonApi/JsonApiResourceTest.php | 131 +++++++++++++++++- 3 files changed, 154 insertions(+), 6 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 67469fd9c656..f0acefc6fa03 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -4,6 +4,8 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\AsPivot; +use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; @@ -138,6 +140,8 @@ protected function compileResourceRelationships(Request $request): void return; } + $this->resource->loadMissing($this->toRelationships($request)); + $this->loadedRelationshipsMap = new WeakMap; $this->loadedRelationshipIdentifiers = (new Collection($this->resource->getRelations())) @@ -158,15 +162,19 @@ protected function compileResourceRelationships(Request $request): void })]]; } + if ($relations instanceof Pivot || in_array(AsPivot::class, class_uses_recursive($relations), true)) { + return [$key => null]; + } + return [$key => ['data' => transform( - [static::resourceTypeFromModel($relations), static::resourceIdFromModel($relations)], - function ($uniqueKey) use ($relations) { - $this->loadedRelationshipsMap[$relations] = $uniqueKey; + [static::resourceTypeFromModel($related), static::resourceIdFromModel($related)], + function ($uniqueKey) use ($related) { + $this->loadedRelationshipsMap[$related] = $uniqueKey; return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]]; } )]]; - })->all(); + })->filter()->all(); } /** @@ -174,6 +182,10 @@ function ($uniqueKey) use ($relations) { */ public function resolveIncludedResources(Request $request): array { + if (! $this->resource instanceof Model) { + return []; + } + $this->compileResourceRelationships($request); $relations = new Collection; diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index b595332790d0..bc1786673cef 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -51,6 +51,15 @@ public static function configure(?string $version = null, array $ext = [], array ]); } + public function toRelationships(Request $request) + { + if (property_exists($this, 'relationships')) { + return $this->relationships; + } + + return []; + } + /** * Get the resource's ID. * diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index c86b3a5bd261..48944a47680a 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -5,18 +5,21 @@ use Illuminate\Database\Eloquent\Attributes\UseResource; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Database\Schema\Blueprint; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\Request; -use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\JsonApi\JsonApiResource; +use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Support\Facades\Schema; +use Orchestra\Testbench\Attributes\WithConfig; use Orchestra\Testbench\Attributes\WithMigration; use Orchestra\Testbench\Factories\UserFactory; use Orchestra\Testbench\TestCase; #[WithMigration] +#[WithConfig('auth.providers.users.model', User::class)] class JsonApiResourceTest extends TestCase { use RefreshDatabase; @@ -51,11 +54,28 @@ protected function afterRefreshingDatabase() { Schema::create('posts', function (Blueprint $table) { $table->id(); - $table->foreignId('user_id'); + $table->foreignId('user_id')->index(); $table->string('title'); $table->text('content'); $table->timestamps(); }); + + Schema::create('teams', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->index(); + $table->string('name'); + $table->boolean('personal_team'); + }); + + Schema::create('team_user', function (Blueprint $table) { + $table->id(); + $table->foreignId('team_id'); + $table->foreignId('user_id'); + $table->string('role')->nullable(); + $table->timestamps(); + + $table->index(['team_id', 'user_id']); + }); } public function testItCanGenerateJsonApiResponse() @@ -103,8 +123,16 @@ public function testItCanGenerateJsonApiResponseWithEmptyRelationship() public function testItCanGenerateJsonApiResponseWithEagerLoadedRelationship() { + $now = $this->freezeSecond(); $user = UserFactory::new()->create(); + $team = TeamFactory::new()->create([ + 'name' => 'Laravel Team', + ]); + + $user->teams()->attach($team, ['role' => 'Admin']); + $user->teams()->attach($team, ['role' => 'Member']); + $posts = PostFactory::new()->times(2)->create([ 'user_id' => $user->getKey(), ]); @@ -126,6 +154,11 @@ public function testItCanGenerateJsonApiResponseWithEagerLoadedRelationship() ['id' => (string) $posts[1]->getKey(), 'type' => 'posts'], ], ], + 'teams' => [ + 'data' => [ + ['id' => (string) $team->getKey(), 'type' => 'teams'], + ], + ], ], ], 'included' => [ @@ -139,6 +172,38 @@ public function testItCanGenerateJsonApiResponseWithEagerLoadedRelationship() 'type' => 'posts', 'attributes' => ['title' => $posts[1]->title, 'content' => $posts[1]->content], ], + [ + 'id' => (string) $team->getKey(), + 'type' => 'teams', + 'attributes' => [ + 'id' => $team->getKey(), + 'membership' => [ + 'created_at' => $now, + 'role' => 'Admin', + 'team_id' => $team->getKey(), + 'user_id' => $user->getKey(), + ], + 'name' => 'Laravel Team', + 'personal_team' => true, + 'user_id' => $team->user_id, + ], + ], + [ + 'id' => (string) $team->getKey(), + 'type' => 'teams', + 'attributes' => [ + 'id' => $team->getKey(), + 'membership' => [ + 'created_at' => $now, + 'role' => 'Member', + 'team_id' => $team->getKey(), + 'user_id' => $user->getKey(), + ], + 'name' => 'Laravel Team', + 'personal_team' => true, + 'user_id' => $team->user_id, + ], + ], ], ]); } @@ -151,6 +216,15 @@ public function posts() { return $this->hasMany(Post::class); } + + public function teams() + { + return $this->belongsToMany(Team::class) + ->withPivot('role') + ->withTimestamps() + ->using(Membership::class) + ->as('membership'); + } } class UserResource extends JsonResource @@ -166,6 +240,10 @@ public function toArray(Request $request) class UserApiResource extends JsonApiResource { + protected array $relationships = [ + 'teams', + ]; + public function toAttributes(Request $request) { return [ @@ -212,3 +290,52 @@ public function modelName() return Post::class; } } + +class Team extends Model +{ + public $timestamps = false; + + protected function casts(): array + { + return [ + 'personal_team' => 'boolean', + ]; + } + + public function users() + { + return $this->belongsToMany(User::class) + ->withPivot('role') + ->withTimestamps() + ->using(Membership::class) + ->as('membership'); + } +} + +class Membership extends Pivot +{ + protected $table = 'team_user'; +} + +class TeamFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => $this->faker->unique()->company(), + 'user_id' => UserFactory::new(), + 'personal_team' => true, + ]; + } + + #[\Override] + public function modelName() + { + return Team::class; + } +} From cd01fa74548d923501f843e4f830ba6d024c3fc9 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Mon, 3 Nov 2025 12:04:59 +0000 Subject: [PATCH 02/34] Apply fixes from StyleCI --- .../Integration/Http/Resources/JsonApi/JsonApiResourceTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index 48944a47680a..a7c0c07b3687 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -10,8 +10,8 @@ use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\Request; -use Illuminate\Http\Resources\JsonApi\JsonApiResource; use Illuminate\Http\Resources\Json\JsonResource; +use Illuminate\Http\Resources\JsonApi\JsonApiResource; use Illuminate\Support\Facades\Schema; use Orchestra\Testbench\Attributes\WithConfig; use Orchestra\Testbench\Attributes\WithMigration; From ec2582509cf12d9ed315b2b4ea9571aecc5d2adc Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Tue, 4 Nov 2025 11:17:57 +0800 Subject: [PATCH 03/34] wip Signed-off-by: Mior Muhammad Zaki --- .../JsonApi/AnonymousResourceCollection.php | 2 +- .../Concerns/ResolvesJsonApiElements.php | 23 ++++++++++++------- .../Resources/JsonApi/JsonApiResourceTest.php | 3 +++ 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php index ebc314652249..5e1a0b10d220 100644 --- a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php +++ b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php @@ -4,6 +4,7 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Arr; class AnonymousResourceCollection extends \Illuminate\Http\Resources\Json\AnonymousResourceCollection { @@ -20,7 +21,6 @@ public function with($request) 'included' => $this->collection ->map(fn ($resource) => $resource->resolveIncludedResources($request)) ->flatten(depth: 1) - ->uniqueStrict(fn ($relation): array => [$relation['id'], $relation['type']]) ->all(), ...($implementation = JsonApiResource::$jsonApiInformation) ? ['jsonapi' => $implementation] diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index f0acefc6fa03..7c56a9cbd5da 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -5,6 +5,7 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\AsPivot; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Http\Request; @@ -151,11 +152,15 @@ protected function compileResourceRelationships(Request $request): void return [$key => ['data' => $relations]]; } + $relationship = $this->resource->{$key}(); + + $isUnique = ! $relationship instanceof BelongsToMany; + $key = static::resourceTypeFromModel($relations->first()); - return [$key => ['data' => $relations->map(function ($relation) use ($key) { - return transform([$key, static::resourceIdFromModel($relation)], function ($uniqueKey) use ($relation) { - $this->loadedRelationshipsMap[$relation] = $uniqueKey; + return [$key => ['data' => $relations->map(function ($relation) use ($key, $isUnique) { + return transform([$key, static::resourceIdFromModel($relation)], function ($uniqueKey) use ($relation, $isUnique) { + $this->loadedRelationshipsMap[$relation] = [...$uniqueKey, $isUnique]; return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]]; }); @@ -167,9 +172,9 @@ protected function compileResourceRelationships(Request $request): void } return [$key => ['data' => transform( - [static::resourceTypeFromModel($related), static::resourceIdFromModel($related)], - function ($uniqueKey) use ($related) { - $this->loadedRelationshipsMap[$related] = $uniqueKey; + [static::resourceTypeFromModel($relations), static::resourceIdFromModel($relations)], + function ($uniqueKey) use ($relations) { + $this->loadedRelationshipsMap[$relations] = [...$uniqueKey, true]; return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]]; } @@ -201,13 +206,15 @@ public function resolveIncludedResources(Request $request): array $relations->push([ 'id' => $uniqueKey[1], 'type' => $uniqueKey[0], + '_uniqueKey' => $uniqueKey[2] === true ? null : (string) Str::random(), 'attributes' => Arr::get($resourceInstance->resolve($request), 'data.attributes', []), ]); } return $relations->uniqueStrict( - fn ($relation): array => [$relation['id'], $relation['type']] - )->all(); + fn ($relation): array => [$relation['id'], $relation['type'], $relation['_uniqueKey']] + )->map(fn ($relation): array => Arr::except($relation, ['_uniqueKey'])) + ->all(); } /** diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index a7c0c07b3687..6926b3b6c853 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -157,6 +157,7 @@ public function testItCanGenerateJsonApiResponseWithEagerLoadedRelationship() 'teams' => [ 'data' => [ ['id' => (string) $team->getKey(), 'type' => 'teams'], + ['id' => (string) $team->getKey(), 'type' => 'teams'], ], ], ], @@ -182,6 +183,7 @@ public function testItCanGenerateJsonApiResponseWithEagerLoadedRelationship() 'role' => 'Admin', 'team_id' => $team->getKey(), 'user_id' => $user->getKey(), + 'updated_at' => $now, ], 'name' => 'Laravel Team', 'personal_team' => true, @@ -198,6 +200,7 @@ public function testItCanGenerateJsonApiResponseWithEagerLoadedRelationship() 'role' => 'Member', 'team_id' => $team->getKey(), 'user_id' => $user->getKey(), + 'updated_at' => $now, ], 'name' => 'Laravel Team', 'personal_team' => true, From 9a0a8fe6bad7350318dee62d729c9629d3a45130 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Tue, 4 Nov 2025 03:18:20 +0000 Subject: [PATCH 04/34] Apply fixes from StyleCI --- .../Http/Resources/JsonApi/AnonymousResourceCollection.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php index 5e1a0b10d220..956166e0c541 100644 --- a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php +++ b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php @@ -4,7 +4,6 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Support\Arr; class AnonymousResourceCollection extends \Illuminate\Http\Resources\Json\AnonymousResourceCollection { From 3ee71cc79769243c6b37cafc169deb022590449c Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Tue, 4 Nov 2025 12:47:54 +0800 Subject: [PATCH 05/34] wip Signed-off-by: Mior Muhammad Zaki --- .../Concerns/ResolvesJsonApiElements.php | 17 +++++++++-------- .../Resources/JsonApi/JsonApiResourceTest.php | 8 ++++++++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 7c56a9cbd5da..b048fe295b34 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -195,7 +195,7 @@ public function resolveIncludedResources(Request $request): array $relations = new Collection; - foreach ($this->loadedRelationshipsMap as $relation => $uniqueKey) { + foreach ($this->loadedRelationshipsMap as $relation => $value) { $resourceInstance = rescue(fn () => $relation->toResource(), new JsonApiResource($relation), false); if (! $resourceInstance instanceof JsonApiResource && @@ -203,18 +203,19 @@ public function resolveIncludedResources(Request $request): array $resourceInstance = new JsonApiResource($resourceInstance->resource); } + [$type, $id, $isUnique] = $value; + $relations->push([ - 'id' => $uniqueKey[1], - 'type' => $uniqueKey[0], - '_uniqueKey' => $uniqueKey[2] === true ? null : (string) Str::random(), + 'id' => $id, + 'type' => $type, + '_uniqueKey' => $isUnique === true ? [$id, $type] : [$id, $type, (string) Str::random()], 'attributes' => Arr::get($resourceInstance->resolve($request), 'data.attributes', []), ]); } - return $relations->uniqueStrict( - fn ($relation): array => [$relation['id'], $relation['type'], $relation['_uniqueKey']] - )->map(fn ($relation): array => Arr::except($relation, ['_uniqueKey'])) - ->all(); + return $relations->uniqueStrict(fn ($relation): array => $relation['_uniqueKey']) + ->map(fn ($relation): array => Arr::except($relation, ['_uniqueKey'])) + ->all(); } /** diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index 6926b3b6c853..607cf082f9af 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -92,6 +92,11 @@ public function testItCanGenerateJsonApiResponse() 'name' => $user->name, 'email' => $user->email, ], + 'relationships' => [ + 'teams' => [ + 'data' => [], + ], + ], ], ]) ->assertJsonMissing(['jsonapi', 'included']); @@ -115,6 +120,9 @@ public function testItCanGenerateJsonApiResponseWithEmptyRelationship() 'posts' => [ 'data' => [], ], + 'teams' => [ + 'data' => [], + ], ], ], ]) From 5a3f43f087f51aac600c61fd441b12a3af39fc81 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Tue, 4 Nov 2025 20:55:14 +0800 Subject: [PATCH 06/34] wip Signed-off-by: Mior Muhammad Zaki --- .../Concerns/ResolvesJsonApiElements.php | 6 +- .../Resources/JsonApi/JsonApiResourceTest.php | 74 +++++++++++++++++-- 2 files changed, 70 insertions(+), 10 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index b048fe295b34..79cf32964292 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -167,18 +167,18 @@ protected function compileResourceRelationships(Request $request): void })]]; } - if ($relations instanceof Pivot || in_array(AsPivot::class, class_uses_recursive($relations), true)) { + if (is_null($relations) || $relations instanceof Pivot || in_array(AsPivot::class, class_uses_recursive($relations), true)) { return [$key => null]; } - return [$key => ['data' => transform( + return [$key => ['data' => [transform( [static::resourceTypeFromModel($relations), static::resourceIdFromModel($relations)], function ($uniqueKey) use ($relations) { $this->loadedRelationshipsMap[$relations] = [...$uniqueKey, true]; return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]]; } - )]]; + )]]]; })->filter()->all(); } diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index 903ca76dab39..ba248ecbb43c 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -60,6 +60,13 @@ protected function afterRefreshingDatabase() $table->timestamps(); }); + Schema::create('profiles', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->unique(); + $table->date('date_of_birth')->nullable(); + $table->string('timezone')->nullable(); + }); + Schema::create('teams', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->index(); @@ -133,6 +140,11 @@ public function testItCanGenerateJsonApiResponseWithEagerLoadedRelationship() { $now = $this->freezeSecond(); $user = UserFactory::new()->create(); + $profile = ProfileFactory::new()->create([ + 'user_id' => $user->getKey(), + 'date_of_birth' => '2011-06-09', + 'timezone' => 'America/Chicago', + ]); $team = TeamFactory::new()->create([ 'name' => 'Laravel Team', @@ -162,6 +174,11 @@ public function testItCanGenerateJsonApiResponseWithEagerLoadedRelationship() ['id' => (string) $posts[1]->getKey(), 'type' => 'posts'], ], ], + 'profile' => [ + 'data' => [ + ['id' => (string) $profile->getKey(), 'type' => 'profiles'], + ], + ], 'teams' => [ 'data' => [ ['id' => (string) $team->getKey(), 'type' => 'teams'], @@ -174,12 +191,28 @@ public function testItCanGenerateJsonApiResponseWithEagerLoadedRelationship() [ 'id' => (string) $posts[0]->getKey(), 'type' => 'posts', - 'attributes' => ['title' => $posts[0]->title, 'content' => $posts[0]->content], + 'attributes' => [ + 'title' => $posts[0]->title, + 'content' => $posts[0]->content, + ], ], [ 'id' => (string) $posts[1]->getKey(), 'type' => 'posts', - 'attributes' => ['title' => $posts[1]->title, 'content' => $posts[1]->content], + 'attributes' => [ + 'title' => $posts[1]->title, + 'content' => $posts[1]->content, + ], + ], + [ + 'id' => (string) $profile->getKey(), + 'type' => 'profiles', + 'attributes' => [ + 'date_of_birth' => '2011-06-09', + 'id' => $profile->getKey(), + 'timezone' => 'America/Chicago', + 'user_id' => 1, + ], ], [ 'id' => (string) $team->getKey(), @@ -223,6 +256,11 @@ public function testItCanGenerateJsonApiResponseWithEagerLoadedRelationship() #[UseResource(UserApiResource::class)] class User extends Authenticatable { + public function profile() + { + return $this->hasOne(Profile::class); + } + public function posts() { return $this->hasMany(Post::class); @@ -252,6 +290,7 @@ public function toArray(Request $request) class UserApiResource extends JsonApiResource { protected array $relationships = [ + 'profile', 'teams', ]; @@ -327,11 +366,6 @@ class Membership extends Pivot class TeamFactory extends Factory { - /** - * Define the model's default state. - * - * @return array - */ public function definition(): array { return [ @@ -347,3 +381,29 @@ public function modelName() return Team::class; } } + +class Profile extends Model +{ + public $timestamps = false; + + public function user() + { + return $this->belongsTo(User::class); + } +} + +class ProfileFactory extends Factory +{ + public function definition(): array + { + return [ + 'user_id' => UserFactory::new(), + ]; + } + + #[\Override] + public function modelName() + { + return Profile::class; + } +} From 6de34f604b56180f6c620951f9162acfa2f56a27 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 4 Nov 2025 10:46:34 -0600 Subject: [PATCH 07/34] Update JsonApiResource.php --- .../Resources/JsonApi/JsonApiResource.php | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index f7911407efa0..1f447da611e3 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -51,15 +51,6 @@ public static function configure(?string $version = null, array $ext = [], array ]); } - public function toRelationships(Request $request) - { - if (property_exists($this, 'relationships')) { - return $this->relationships; - } - - return []; - } - /** * Get the resource's ID. * @@ -96,6 +87,21 @@ public function toAttributes(Request $request) return $this->toArray($request); } + /** + * Get the resource's relationships. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Contracts\Support\Arrayable|array + */ + public function toRelationships(Request $request) + { + if (property_exists($this, 'relationships')) { + return $this->relationships; + } + + return []; + } + /** * Get the resource's links. * From 04e0d2e2ed72ee6ae33da6f2fbe7f11612a11cff Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 4 Nov 2025 11:02:33 -0600 Subject: [PATCH 08/34] only include relationship if defined --- .../JsonApi/Concerns/ResolvesJsonApiElements.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 79cf32964292..31759eea05f5 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -141,11 +141,17 @@ protected function compileResourceRelationships(Request $request): void return; } - $this->resource->loadMissing($this->toRelationships($request)); + $this->resource->loadMissing( + $resourceRelationships = $this->toRelationships($request) + ); $this->loadedRelationshipsMap = new WeakMap; - $this->loadedRelationshipIdentifiers = (new Collection($this->resource->getRelations())) + $resourceRelationshipKeys = array_is_list($resourceRelationships) + ? array_flip($resourceRelationships) + : array_flip(array_keys($resourceRelationships)); + + $this->loadedRelationshipIdentifiers = (new Collection(array_intersect_key($this->resource->getRelations(), $resourceRelationshipKeys))) ->mapWithKeys(function ($relations, $key) { if ($relations instanceof Collection) { if ($relations->isEmpty()) { From a89fd69fe7d75410f6b67f16856021f09a0e449e Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 4 Nov 2025 11:03:25 -0600 Subject: [PATCH 09/34] 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 31759eea05f5..72dbeb750bf2 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -145,12 +145,12 @@ protected function compileResourceRelationships(Request $request): void $resourceRelationships = $this->toRelationships($request) ); - $this->loadedRelationshipsMap = new WeakMap; - $resourceRelationshipKeys = array_is_list($resourceRelationships) ? array_flip($resourceRelationships) : array_flip(array_keys($resourceRelationships)); + $this->loadedRelationshipsMap = new WeakMap; + $this->loadedRelationshipIdentifiers = (new Collection(array_intersect_key($this->resource->getRelations(), $resourceRelationshipKeys))) ->mapWithKeys(function ($relations, $key) { if ($relations instanceof Collection) { From b0eaf030dd54a450063de35f8fa0c1f726967e04 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 4 Nov 2025 11:20:04 -0600 Subject: [PATCH 10/34] tweak how relations work --- .../Resources/JsonApi/Concerns/ResolvesJsonApiElements.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 72dbeb750bf2..55a07b237b65 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -141,14 +141,14 @@ protected function compileResourceRelationships(Request $request): void return; } - $this->resource->loadMissing( - $resourceRelationships = $this->toRelationships($request) - ); + $resourceRelationships = $this->toRelationships($request); $resourceRelationshipKeys = array_is_list($resourceRelationships) ? array_flip($resourceRelationships) : array_flip(array_keys($resourceRelationships)); + $this->resource->loadMissing($resourceRelationshipKeys); + $this->loadedRelationshipsMap = new WeakMap; $this->loadedRelationshipIdentifiers = (new Collection(array_intersect_key($this->resource->getRelations(), $resourceRelationshipKeys))) From 04bfaadcad3afd6d4da56cb0f2f23b61da16ef14 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 4 Nov 2025 13:14:19 -0600 Subject: [PATCH 11/34] work on relationships --- .../Concerns/ResolvesJsonApiElements.php | 63 +++++++++++-------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 55a07b237b65..6f99cdec77ef 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -151,41 +151,50 @@ protected function compileResourceRelationships(Request $request): void $this->loadedRelationshipsMap = new WeakMap; - $this->loadedRelationshipIdentifiers = (new Collection(array_intersect_key($this->resource->getRelations(), $resourceRelationshipKeys))) - ->mapWithKeys(function ($relations, $key) { - if ($relations instanceof Collection) { - if ($relations->isEmpty()) { - return [$key => ['data' => $relations]]; - } + $this->loadedRelationshipIdentifiers = (new Collection( + array_is_list($resourceRelationships) + ? array_intersect_key($this->resource->getRelations(), $resourceRelationshipKeys) + : $resourceRelationships + ))->mapWithKeys(function ($relations, $key) { + $relations = value($relations); + + if ($relations instanceof Collection) { + $relations = $relations->values(); + + if ($relations->isEmpty()) { + return [$key => ['data' => $relations]]; + } - $relationship = $this->resource->{$key}(); + $relationship = $this->resource->{$key}(); - $isUnique = ! $relationship instanceof BelongsToMany; + $isUnique = ! $relationship instanceof BelongsToMany; - $key = static::resourceTypeFromModel($relations->first()); + $key = static::resourceTypeFromModel($relations->first()); - return [$key => ['data' => $relations->map(function ($relation) use ($key, $isUnique) { - return transform([$key, static::resourceIdFromModel($relation)], function ($uniqueKey) use ($relation, $isUnique) { - $this->loadedRelationshipsMap[$relation] = [...$uniqueKey, $isUnique]; + return [$key => ['data' => $relations->map(function ($relation) use ($key, $isUnique) { + return transform([$key, static::resourceIdFromModel($relation)], function ($uniqueKey) use ($relation, $isUnique) { + $this->loadedRelationshipsMap[$relation] = [...$uniqueKey, $isUnique]; - return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]]; - }); - })]]; - } + return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]]; + }); + })]]; + } - if (is_null($relations) || $relations instanceof Pivot || in_array(AsPivot::class, class_uses_recursive($relations), true)) { - return [$key => null]; - } + if (is_null($relations) || + $relations instanceof Pivot || + in_array(AsPivot::class, class_uses_recursive($relations), true)) { + return [$key => null]; + } - return [$key => ['data' => [transform( - [static::resourceTypeFromModel($relations), static::resourceIdFromModel($relations)], - function ($uniqueKey) use ($relations) { - $this->loadedRelationshipsMap[$relations] = [...$uniqueKey, true]; + return [$key => ['data' => [transform( + [static::resourceTypeFromModel($relations), static::resourceIdFromModel($relations)], + function ($uniqueKey) use ($relations) { + $this->loadedRelationshipsMap[$relations] = [...$uniqueKey, true]; - return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]]; - } - )]]]; - })->filter()->all(); + return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]]; + } + )]]]; + })->filter()->all(); } /** From 2479bae9b6d4589afd7f70c5f8f74fbffbf9a621 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 5 Nov 2025 10:23:28 +0800 Subject: [PATCH 12/34] fixes implementation Signed-off-by: Mior Muhammad Zaki --- .../Resources/JsonApi/Concerns/ResolvesJsonApiElements.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 6f99cdec77ef..42ad5104f91a 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -144,7 +144,7 @@ protected function compileResourceRelationships(Request $request): void $resourceRelationships = $this->toRelationships($request); $resourceRelationshipKeys = array_is_list($resourceRelationships) - ? array_flip($resourceRelationships) + ? $resourceRelationships : array_flip(array_keys($resourceRelationships)); $this->resource->loadMissing($resourceRelationshipKeys); @@ -228,8 +228,8 @@ public function resolveIncludedResources(Request $request): array ]); } - return $relations->uniqueStrict(fn ($relation): array => $relation['_uniqueKey']) - ->map(fn ($relation): array => Arr::except($relation, ['_uniqueKey'])) + return $relations->uniqueStrict(fn ($relation) => $relation['_uniqueKey']) + ->map(fn ($relation) => Arr::except($relation, ['_uniqueKey'])) ->all(); } From f2ffda03c990559b09e8fdf617b055199e243b2a Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 5 Nov 2025 10:23:47 +0800 Subject: [PATCH 13/34] Add `JsonApiRequest` Signed-off-by: Mior Muhammad Zaki --- .../Http/Resources/Json/JsonResource.php | 16 ++++++++-- .../JsonApi/AnonymousResourceCollection.php | 14 +++++++++ .../Concerns/ResolvesJsonApiElements.php | 20 ++++++------- .../Concerns/ResolvesJsonApiRequest.php | 26 +++++++++++++++++ .../Http/Resources/JsonApi/JsonApiRequest.php | 10 +++++++ .../Resources/JsonApi/JsonApiResource.php | 29 +++++++++++++++++-- .../Resources/JsonApi/JsonApiResourceTest.php | 13 --------- 7 files changed, 100 insertions(+), 28 deletions(-) create mode 100644 src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiRequest.php create mode 100644 src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php diff --git a/src/Illuminate/Http/Resources/Json/JsonResource.php b/src/Illuminate/Http/Resources/Json/JsonResource.php index f55fc71461e7..14e032a4a1f4 100644 --- a/src/Illuminate/Http/Resources/Json/JsonResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonResource.php @@ -112,7 +112,7 @@ protected static function newCollection($resource) public function resolve($request = null) { $data = $this->toAttributes( - $request ?: Container::getInstance()->make('request') + $request ?: $this->resolveRequestFromContainer() ); if ($data instanceof Arrayable) { @@ -260,7 +260,7 @@ public static function withoutWrapping() public function response($request = null) { return $this->toResponse( - $request ?: Container::getInstance()->make('request') + $request ?: $this->resolveRequestFromContainer() ); } @@ -282,7 +282,17 @@ public function toResponse($request) */ public function jsonSerialize(): array { - return $this->resolve(Container::getInstance()->make('request')); + return $this->resolve($this->resolveRequestFromContainer()); + } + + /** + * Resolve the Request instance from Container. + * + * @return \Illuminate\Http\Request + */ + protected function resolveRequestFromContainer() + { + return Container::getInstance()->make('request'); } /** diff --git a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php index 956166e0c541..e35e8fb2734d 100644 --- a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php +++ b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php @@ -2,11 +2,14 @@ namespace Illuminate\Http\Resources\JsonApi; +use Illuminate\Container\Container; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; class AnonymousResourceCollection extends \Illuminate\Http\Resources\Json\AnonymousResourceCollection { + use Concerns\ResolvesJsonApiRequest; + /** * Get any additional data that should be returned with the resource array. * @@ -53,4 +56,15 @@ public function withResponse(Request $request, JsonResponse $response): void { $response->header('Content-Type', 'application/vnd.api+json'); } + + /** + * Resolve the Request instance from Container. + * + * @return \Illuminate\Http\Resources\JsonApi\SparseRequest + */ + #[\Override] + protected function resolveRequestFromContainer() + { + return $this->resolveJsonApiRequestFrom(Container::getInstance()->make('request')); + } } diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 42ad5104f91a..9fcd988091e7 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -8,8 +8,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Database\Eloquent\Relations\Relation; -use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; +use Illuminate\Http\Resources\JsonApi\JsonApiRequest; use Illuminate\Http\Resources\JsonApi\Exceptions\ResourceIdentificationException; use Illuminate\Http\Resources\JsonApi\JsonApiResource; use Illuminate\Support\Arr; @@ -35,7 +35,7 @@ trait ResolvesJsonApiElements /** * Resolves `data` for the resource. */ - public function resolveResourceData(Request $request): array + public function resolveResourceData(JsonApiRequest $request): array { return [ 'id' => $this->resolveResourceIdentifier($request), @@ -56,7 +56,7 @@ public function resolveResourceData(Request $request): array * * @throws ResourceIdentificationException */ - protected function resolveResourceIdentifier(Request $request): string + protected function resolveResourceIdentifier(JsonApiRequest $request): string { if (! is_null($resourceId = $this->toId($request))) { return $resourceId; @@ -75,7 +75,7 @@ protected function resolveResourceIdentifier(Request $request): string * * @throws ResourceIdentificationException */ - protected function resolveResourceType(Request $request): string + protected function resolveResourceType(JsonApiRequest $request): string { if (! is_null($resourceType = $this->toType($request))) { return $resourceType; @@ -94,7 +94,7 @@ protected function resolveResourceType(Request $request): string * * @throws \RuntimeException */ - protected function resolveResourceAttributes(Request $request): array + protected function resolveResourceAttributes(JsonApiRequest $request): array { $data = $this->toAttributes($request); @@ -119,7 +119,7 @@ protected function resolveResourceAttributes(Request $request): array * * @throws \RuntimeException */ - protected function resolveResourceRelationshipIdentifiers(Request $request): array + protected function resolveResourceRelationshipIdentifiers(JsonApiRequest $request): array { if (! $this->resource instanceof Model) { return []; @@ -135,7 +135,7 @@ protected function resolveResourceRelationshipIdentifiers(Request $request): arr /** * Compile resource relationships. */ - protected function compileResourceRelationships(Request $request): void + protected function compileResourceRelationships(JsonApiRequest $request): void { if ($this->loadedRelationshipsMap instanceof WeakMap) { return; @@ -200,7 +200,7 @@ function ($uniqueKey) use ($relations) { /** * Resolves `included` for the resource. */ - public function resolveIncludedResources(Request $request): array + public function resolveIncludedResources(JsonApiRequest $request): array { if (! $this->resource instanceof Model) { return []; @@ -238,7 +238,7 @@ public function resolveIncludedResources(Request $request): array * * @return array */ - protected function resolveResourceLinks(Request $request): array + protected function resolveResourceLinks(JsonApiRequest $request): array { return $this->toLinks($request); } @@ -248,7 +248,7 @@ protected function resolveResourceLinks(Request $request): array * * @return array */ - protected function resolveResourceMetaInformation(Request $request): array + protected function resolveResourceMetaInformation(JsonApiRequest $request): array { return $this->toMeta($request); } diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiRequest.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiRequest.php new file mode 100644 index 000000000000..5b2563554274 --- /dev/null +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiRequest.php @@ -0,0 +1,26 @@ + $this->resolveResourceData($request), + 'data' => $this->resolveResourceData($this->resolveJsonApiRequestFrom($request)), ]; } + /** + * Create an HTTP response that represents the object. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + #[\Override] + public function toResponse($request) + { + return parent::toResponse($this->resolveJsonApiRequestFrom($request)); + } + /** * Customize the outgoing response for the resource. */ @@ -162,6 +176,17 @@ public function withResponse(Request $request, JsonResponse $response): void $response->header('Content-Type', 'application/vnd.api+json'); } + /** + * Resolve the Request instance from Container. + * + * @return \Illuminate\Http\Resources\JsonApi\SparseRequest + */ + #[\Override] + protected function resolveRequestFromContainer() + { + return $this->resolveJsonApiRequestFrom(Container::getInstance()->make('request')); + } + /** * Create a new resource collection instance. * diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index ba248ecbb43c..1346c24321fa 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -99,11 +99,6 @@ public function testItCanGenerateJsonApiResponse() 'name' => $user->name, 'email' => $user->email, ], - 'relationships' => [ - 'teams' => [ - 'data' => [], - ], - ], ], ]) ->assertJsonMissing(['jsonapi', 'included']); @@ -123,14 +118,6 @@ public function testItCanGenerateJsonApiResponseWithEmptyRelationship() 'name' => $user->name, 'email' => $user->email, ], - 'relationships' => [ - 'posts' => [ - 'data' => [], - ], - 'teams' => [ - 'data' => [], - ], - ], ], ]) ->assertJsonMissing(['jsonapi', 'included']); From 04fc800fd73b359b87b06119be2f25791d4d824b Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 5 Nov 2025 02:24:26 +0000 Subject: [PATCH 14/34] Apply fixes from StyleCI --- .../Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 9fcd988091e7..ead4f28aadb9 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -9,8 +9,8 @@ use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Http\Resources\Json\JsonResource; -use Illuminate\Http\Resources\JsonApi\JsonApiRequest; use Illuminate\Http\Resources\JsonApi\Exceptions\ResourceIdentificationException; +use Illuminate\Http\Resources\JsonApi\JsonApiRequest; use Illuminate\Http\Resources\JsonApi\JsonApiResource; use Illuminate\Support\Arr; use Illuminate\Support\Collection; From 2e56f9759f0e3f5a66ccf1daa73d22a2666560f4 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 5 Nov 2025 10:27:14 +0800 Subject: [PATCH 15/34] wip Signed-off-by: Mior Muhammad Zaki --- src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index 08334352b849..a16bdb7a9200 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -3,11 +3,9 @@ namespace Illuminate\Http\Resources\JsonApi; use BadMethodCallException; -use Illuminate\Container\Container; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; -use Illuminate\Support\Collection; class JsonApiResource extends JsonResource { @@ -151,7 +149,7 @@ public function with($request) public function resolve($request = null) { return [ - 'data' => $this->resolveResourceData($this->resolveJsonApiRequestFrom($request)), + 'data' => $this->resolveResourceData($this->resolveJsonApiRequestFrom($request ?? $this->resolveRequestFromContainer())), ]; } @@ -179,12 +177,12 @@ public function withResponse(Request $request, JsonResponse $response): void /** * Resolve the Request instance from Container. * - * @return \Illuminate\Http\Resources\JsonApi\SparseRequest + * @return \Illuminate\Http\Resources\JsonApi\JsonApiRequest */ #[\Override] protected function resolveRequestFromContainer() { - return $this->resolveJsonApiRequestFrom(Container::getInstance()->make('request')); + return $this->resolveJsonApiRequestFrom(parent::resolveRequestFromContainer()); } /** From 334b9c28c009daaa94ae53e1dcd42c19a39ad78e Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 5 Nov 2025 13:26:12 +0800 Subject: [PATCH 16/34] wip Signed-off-by: Mior Muhammad Zaki --- .../Concerns/ResolvesJsonApiElements.php | 11 +++++--- .../Http/Resources/JsonApi/JsonApiRequest.php | 17 +++++++++++- .../Resources/JsonApi/JsonApiResourceTest.php | 26 ++++++++++++++----- 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index ead4f28aadb9..8448ed419f49 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -37,11 +37,13 @@ trait ResolvesJsonApiElements */ public function resolveResourceData(JsonApiRequest $request): array { + $resourceType = $this->resolveResourceType($request); + return [ 'id' => $this->resolveResourceIdentifier($request), - 'type' => $this->resolveResourceType($request), + 'type' => $resourceType, ...(new Collection([ - 'attributes' => $this->resolveResourceAttributes($request), + 'attributes' => $this->resolveResourceAttributes($request, $resourceType), 'relationships' => $this->resolveResourceRelationshipIdentifiers($request), 'links' => $this->resolveResourceLinks($request), 'meta' => $this->resolveResourceMetaInformation($request), @@ -94,7 +96,7 @@ protected function resolveResourceType(JsonApiRequest $request): string * * @throws \RuntimeException */ - protected function resolveResourceAttributes(JsonApiRequest $request): array + protected function resolveResourceAttributes(JsonApiRequest $request, string $resourceType): array { $data = $this->toAttributes($request); @@ -104,8 +106,11 @@ protected function resolveResourceAttributes(JsonApiRequest $request): array $data = $data->jsonSerialize(); } + $sparseFieldsets = $request->sparseFields($resourceType); + $data = (new Collection($data)) ->mapWithKeys(fn ($value, $key) => is_int($key) ? [$value => $this->resource->{$value}] : [$key => $value]) + ->when(! empty($sparseFieldsets), fn ($attributes) => $attributes->only($sparseFieldsets)) ->transform(fn ($value) => value($value, $request)) ->all(); diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php index a4e5ed235e97..de068c14bec9 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php @@ -3,8 +3,23 @@ namespace Illuminate\Http\Resources\JsonApi; use Illuminate\Http\Request; +use Illuminate\Support\Arr; class JsonApiRequest extends Request { - // + public function sparseIncluded(): array + { + return explode(',', (string) $this->string('included', '')); + } + + public function sparseFields(string $key): array + { + $fieldsets = Arr::get($this->array('fields', []), $key, ''); + + if (empty($fieldsets)) { + return []; + } + + return explode(',', $fieldsets); + } } diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index 1346c24321fa..ea8e435c713a 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -39,13 +39,7 @@ protected function tearDown(): void protected function defineRoutes($router) { $router->get('users/{userId}', function (Request $request, $userId) { - $user = User::find($userId); - - if (! empty($includes = $request->array('includes'))) { - $user->loadMissing($includes); - } - - return $user->toResource(); + return User::find($userId)->toResource(); }); } @@ -104,6 +98,24 @@ public function testItCanGenerateJsonApiResponse() ->assertJsonMissing(['jsonapi', 'included']); } + public function testItCanGenerateJsonApiResponseWithSparseFieldsets() + { + $user = UserFactory::new()->create(); + + $this->getJson('/users/'.$user->getKey().'?fields[users]=name') + ->assertHeader('Content-type', 'application/vnd.api+json') + ->assertExactJson([ + 'data' => [ + 'id' => (string) $user->getKey(), + 'type' => 'users', + 'attributes' => [ + 'name' => $user->name, + ], + ], + ]) + ->assertJsonMissing(['jsonapi', 'included']); + } + public function testItCanGenerateJsonApiResponseWithEmptyRelationship() { $user = UserFactory::new()->create(); From 5930c7d9a1e1b2bba60bd04493404cc54f6ad689 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 5 Nov 2025 20:48:49 +0800 Subject: [PATCH 17/34] wip Signed-off-by: Mior Muhammad Zaki --- .../Concerns/ResolvesJsonApiElements.php | 20 +++++++-------- .../Http/Resources/JsonApi/JsonApiRequest.php | 8 +++++- .../Resources/JsonApi/JsonApiResourceTest.php | 25 ++++++++++--------- 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 8448ed419f49..79f467eb0770 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -146,22 +146,20 @@ protected function compileResourceRelationships(JsonApiRequest $request): void return; } - $resourceRelationships = $this->toRelationships($request); + $sparseIncluded = $request->sparseIncluded(); - $resourceRelationshipKeys = array_is_list($resourceRelationships) - ? $resourceRelationships - : array_flip(array_keys($resourceRelationships)); + $resourceRelationships = (new Collection($this->toRelationships($request))) + ->mapWithKeys(fn ($value, $key) => is_int($key) ? [$value => fn () => $this->resource->{$value}] : [$key => $value]) + ->filter(fn ($value, $key) => in_array($key, $sparseIncluded)); - $this->resource->loadMissing($resourceRelationshipKeys); + $resourceRelationshipKeys = $resourceRelationships->keys(); + + $this->resource->loadMissing($resourceRelationshipKeys->all()); $this->loadedRelationshipsMap = new WeakMap; - $this->loadedRelationshipIdentifiers = (new Collection( - array_is_list($resourceRelationships) - ? array_intersect_key($this->resource->getRelations(), $resourceRelationshipKeys) - : $resourceRelationships - ))->mapWithKeys(function ($relations, $key) { - $relations = value($relations); + $this->loadedRelationshipIdentifiers = $resourceRelationships->mapWithKeys(function ($relationResolver, $key) { + $relations = value($relationResolver); if ($relations instanceof Collection) { $relations = $relations->values(); diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php index de068c14bec9..3105c166b681 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php @@ -9,7 +9,13 @@ class JsonApiRequest extends Request { public function sparseIncluded(): array { - return explode(',', (string) $this->string('included', '')); + $included = (string) $this->string('include', ''); + + if (empty($included)) { + return []; + } + + return explode(',', $included); } public function sparseFields(string $key): array diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index ea8e435c713a..5442bb255a34 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -102,7 +102,7 @@ public function testItCanGenerateJsonApiResponseWithSparseFieldsets() { $user = UserFactory::new()->create(); - $this->getJson('/users/'.$user->getKey().'?fields[users]=name') + $this->getJson('/users/'.$user->getKey().'?'.http_build_query(['fields' => ['users' => 'name']])) ->assertHeader('Content-type', 'application/vnd.api+json') ->assertExactJson([ 'data' => [ @@ -156,7 +156,7 @@ public function testItCanGenerateJsonApiResponseWithEagerLoadedRelationship() 'user_id' => $user->getKey(), ]); - $this->getJson('/users/'.$user->getKey().'?'.http_build_query(['includes' => ['posts']])) + $this->getJson('/users/'.$user->getKey().'?'.http_build_query(['include' => 'profile,posts,teams'])) ->assertHeader('Content-type', 'application/vnd.api+json') ->assertExactJson([ 'data' => [ @@ -187,6 +187,16 @@ public function testItCanGenerateJsonApiResponseWithEagerLoadedRelationship() ], ], 'included' => [ + [ + 'id' => (string) $profile->getKey(), + 'type' => 'profiles', + 'attributes' => [ + 'date_of_birth' => '2011-06-09', + 'id' => $profile->getKey(), + 'timezone' => 'America/Chicago', + 'user_id' => 1, + ], + ], [ 'id' => (string) $posts[0]->getKey(), 'type' => 'posts', @@ -203,16 +213,6 @@ public function testItCanGenerateJsonApiResponseWithEagerLoadedRelationship() 'content' => $posts[1]->content, ], ], - [ - 'id' => (string) $profile->getKey(), - 'type' => 'profiles', - 'attributes' => [ - 'date_of_birth' => '2011-06-09', - 'id' => $profile->getKey(), - 'timezone' => 'America/Chicago', - 'user_id' => 1, - ], - ], [ 'id' => (string) $team->getKey(), 'type' => 'teams', @@ -290,6 +290,7 @@ class UserApiResource extends JsonApiResource { protected array $relationships = [ 'profile', + 'posts', 'teams', ]; From 082865516c872eccb3c3e5e5b98963184df3af4f Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 5 Nov 2025 20:52:09 +0800 Subject: [PATCH 18/34] wip Signed-off-by: Mior Muhammad Zaki --- src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php index 3105c166b681..01931d03a1e2 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php @@ -20,7 +20,7 @@ public function sparseIncluded(): array public function sparseFields(string $key): array { - $fieldsets = Arr::get($this->array('fields', []), $key, ''); + $fieldsets = Arr::get($this->array('fields'), $key, ''); if (empty($fieldsets)) { return []; From 4a09643a08f0378ec41a1d05d279ffcb93b97ef6 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 5 Nov 2025 16:29:48 -0600 Subject: [PATCH 19/34] formatting --- .../Http/Resources/Json/JsonResource.php | 20 +++++------ .../JsonApi/AnonymousResourceCollection.php | 2 +- .../Concerns/ResolvesJsonApiElements.php | 34 +++++++++++-------- .../Concerns/ResolvesJsonApiRequest.php | 13 +++---- .../Http/Resources/JsonApi/JsonApiRequest.php | 30 ++++++++-------- .../Resources/JsonApi/JsonApiResource.php | 20 +++++------ 6 files changed, 60 insertions(+), 59 deletions(-) diff --git a/src/Illuminate/Http/Resources/Json/JsonResource.php b/src/Illuminate/Http/Resources/Json/JsonResource.php index 14e032a4a1f4..02ebce156d8c 100644 --- a/src/Illuminate/Http/Resources/Json/JsonResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonResource.php @@ -230,6 +230,16 @@ public function withResponse(Request $request, JsonResponse $response) // } + /** + * Resolve the HTTP request instance from container. + * + * @return \Illuminate\Http\Request + */ + protected function resolveRequestFromContainer() + { + return Container::getInstance()->make('request'); + } + /** * Set the string that should wrap the outer-most resource array. * @@ -285,16 +295,6 @@ public function jsonSerialize(): array return $this->resolve($this->resolveRequestFromContainer()); } - /** - * Resolve the Request instance from Container. - * - * @return \Illuminate\Http\Request - */ - protected function resolveRequestFromContainer() - { - return Container::getInstance()->make('request'); - } - /** * Flush the resource's global state. * diff --git a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php index e35e8fb2734d..d6e3c1a50b0c 100644 --- a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php +++ b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php @@ -58,7 +58,7 @@ public function withResponse(Request $request, JsonResponse $response): void } /** - * Resolve the Request instance from Container. + * Resolve the HTTP request instance from container. * * @return \Illuminate\Http\Resources\JsonApi\SparseRequest */ diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 79f467eb0770..a024dc1c0fab 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -106,11 +106,11 @@ protected function resolveResourceAttributes(JsonApiRequest $request, string $re $data = $data->jsonSerialize(); } - $sparseFieldsets = $request->sparseFields($resourceType); + $sparseFieldset = $request->sparseFields($resourceType); $data = (new Collection($data)) ->mapWithKeys(fn ($value, $key) => is_int($key) ? [$value => $this->resource->{$value}] : [$key => $value]) - ->when(! empty($sparseFieldsets), fn ($attributes) => $attributes->only($sparseFieldsets)) + ->when(! empty($sparseFieldset), fn ($attributes) => $attributes->only($sparseFieldset)) ->transform(fn ($value) => value($value, $request)) ->all(); @@ -159,22 +159,23 @@ protected function compileResourceRelationships(JsonApiRequest $request): void $this->loadedRelationshipsMap = new WeakMap; $this->loadedRelationshipIdentifiers = $resourceRelationships->mapWithKeys(function ($relationResolver, $key) { - $relations = value($relationResolver); + $relatedModels = value($relationResolver); - if ($relations instanceof Collection) { - $relations = $relations->values(); + // Relationship is a collection of models... + if ($relatedModels instanceof Collection) { + $relatedModels = $relatedModels->values(); - if ($relations->isEmpty()) { - return [$key => ['data' => $relations]]; + if ($relatedModels->isEmpty()) { + return [$key => ['data' => $relatedModels]]; } $relationship = $this->resource->{$key}(); $isUnique = ! $relationship instanceof BelongsToMany; - $key = static::resourceTypeFromModel($relations->first()); + $key = static::resourceTypeFromModel($relatedModels->first()); - return [$key => ['data' => $relations->map(function ($relation) use ($key, $isUnique) { + return [$key => ['data' => $relatedModels->map(function ($relation) use ($key, $isUnique) { return transform([$key, static::resourceIdFromModel($relation)], function ($uniqueKey) use ($relation, $isUnique) { $this->loadedRelationshipsMap[$relation] = [...$uniqueKey, $isUnique]; @@ -183,16 +184,19 @@ protected function compileResourceRelationships(JsonApiRequest $request): void })]]; } - if (is_null($relations) || - $relations instanceof Pivot || - in_array(AsPivot::class, class_uses_recursive($relations), true)) { + // Relationship is a single model... + $relatedModel = $relatedModels; + + if (is_null($relatedModel) || + $relatedModel instanceof Pivot || + in_array(AsPivot::class, class_uses_recursive($relatedModel), true)) { return [$key => null]; } return [$key => ['data' => [transform( - [static::resourceTypeFromModel($relations), static::resourceIdFromModel($relations)], - function ($uniqueKey) use ($relations) { - $this->loadedRelationshipsMap[$relations] = [...$uniqueKey, true]; + [static::resourceTypeFromModel($relatedModel), static::resourceIdFromModel($relatedModel)], + function ($uniqueKey) use ($relatedModel) { + $this->loadedRelationshipsMap[$relatedModel] = [...$uniqueKey, true]; return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]]; } diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiRequest.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiRequest.php index 5b2563554274..c24dd927d925 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiRequest.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiRequest.php @@ -2,25 +2,20 @@ namespace Illuminate\Http\Resources\JsonApi\Concerns; -use Illuminate\Container\Container; use Illuminate\Http\Request; use Illuminate\Http\Resources\JsonApi\JsonApiRequest; trait ResolvesJsonApiRequest { /** - * Resolve the Request instance from Container. + * Resolve a JSON API request instance from the given HTTP request. * * @return \Illuminate\Http\Resources\JsonApi\JsonApiRequest */ protected function resolveJsonApiRequestFrom(Request $request) { - if ($request instanceof JsonApiRequest) { - return $request; - } - - $app = Container::getInstance(); - - return JsonApiRequest::createFrom($request); + return $request instanceof JsonApiRequest + ? $request + : JsonApiRequest::createFrom($request); } } diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php index 01931d03a1e2..c5f0f194b1bb 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php @@ -7,25 +7,27 @@ class JsonApiRequest extends Request { - public function sparseIncluded(): array + /** + * Get the request's included fields. + */ + public function sparseFields(string $key): array { - $included = (string) $this->string('include', ''); - - if (empty($included)) { - return []; - } + $fieldsets = Arr::get($this->array('fields'), $key, ''); - return explode(',', $included); + return empty($fieldsets) + ? [] + : explode(',', $fieldsets); } - public function sparseFields(string $key): array + /** + * Get the request's included relationships. + */ + public function sparseIncluded(): array { - $fieldsets = Arr::get($this->array('fields'), $key, ''); - - if (empty($fieldsets)) { - return []; - } + $included = (string) $this->string('include', ''); - return explode(',', $fieldsets); + return empty($included) + ? [] + : explode(',', $included); } } diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index a16bdb7a9200..003ffa41a6d1 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -154,28 +154,28 @@ public function resolve($request = null) } /** - * Create an HTTP response that represents the object. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\JsonResponse + * Customize the outgoing response for the resource. */ #[\Override] - public function toResponse($request) + public function withResponse(Request $request, JsonResponse $response): void { - return parent::toResponse($this->resolveJsonApiRequestFrom($request)); + $response->header('Content-Type', 'application/vnd.api+json'); } /** - * Customize the outgoing response for the resource. + * Create an HTTP response that represents the object. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse */ #[\Override] - public function withResponse(Request $request, JsonResponse $response): void + public function toResponse($request) { - $response->header('Content-Type', 'application/vnd.api+json'); + return parent::toResponse($this->resolveJsonApiRequestFrom($request)); } /** - * Resolve the Request instance from Container. + * Resolve the HTTP request instance from container. * * @return \Illuminate\Http\Resources\JsonApi\JsonApiRequest */ From 2b7861e717f24820fdf7c72e9bca6ac763889b4a Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 6 Nov 2025 14:00:59 +0800 Subject: [PATCH 20/34] wip Signed-off-by: Mior Muhammad Zaki --- .../Resources/JsonApi/Fixtures/Membership.php | 10 + .../Http/Resources/JsonApi/Fixtures/Post.php | 20 ++ .../JsonApi/Fixtures/PostApiResource.php | 13 + .../JsonApi/Fixtures/PostFactory.php | 24 ++ .../Resources/JsonApi/Fixtures/Profile.php | 20 ++ .../JsonApi/Fixtures/ProfileFactory.php | 22 ++ .../Http/Resources/JsonApi/Fixtures/Team.php | 31 +++ .../JsonApi/Fixtures/TeamFactory.php | 24 ++ .../Http/Resources/JsonApi/Fixtures/User.php | 35 +++ .../JsonApi/Fixtures/UserApiResource.php | 23 ++ .../JsonApi/Fixtures/UserResource.php | 17 ++ .../Resources/JsonApi/Fixtures/migrations.php | 36 +++ .../Resources/JsonApi/JsonApiResourceTest.php | 224 +----------------- .../Http/Resources/JsonApi/TestCase.php | 43 ++++ 14 files changed, 330 insertions(+), 212 deletions(-) create mode 100644 tests/Integration/Http/Resources/JsonApi/Fixtures/Membership.php create mode 100644 tests/Integration/Http/Resources/JsonApi/Fixtures/Post.php create mode 100644 tests/Integration/Http/Resources/JsonApi/Fixtures/PostApiResource.php create mode 100644 tests/Integration/Http/Resources/JsonApi/Fixtures/PostFactory.php create mode 100644 tests/Integration/Http/Resources/JsonApi/Fixtures/Profile.php create mode 100644 tests/Integration/Http/Resources/JsonApi/Fixtures/ProfileFactory.php create mode 100644 tests/Integration/Http/Resources/JsonApi/Fixtures/Team.php create mode 100644 tests/Integration/Http/Resources/JsonApi/Fixtures/TeamFactory.php create mode 100644 tests/Integration/Http/Resources/JsonApi/Fixtures/User.php create mode 100644 tests/Integration/Http/Resources/JsonApi/Fixtures/UserApiResource.php create mode 100644 tests/Integration/Http/Resources/JsonApi/Fixtures/UserResource.php create mode 100644 tests/Integration/Http/Resources/JsonApi/Fixtures/migrations.php create mode 100644 tests/Integration/Http/Resources/JsonApi/TestCase.php diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/Membership.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/Membership.php new file mode 100644 index 000000000000..c3af1de45292 --- /dev/null +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/Membership.php @@ -0,0 +1,10 @@ +belongsTo(User::class); + } +} diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/PostApiResource.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/PostApiResource.php new file mode 100644 index 000000000000..6309e6cc5c91 --- /dev/null +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/PostApiResource.php @@ -0,0 +1,13 @@ + UserFactory::new(), + 'title' => $this->faker->word(), + 'content' => $this->faker->words(10, true), + ]; + } + + #[\Override] + public function modelName() + { + return Post::class; + } +} diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/Profile.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/Profile.php new file mode 100644 index 000000000000..d8c29e2fa5b1 --- /dev/null +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/Profile.php @@ -0,0 +1,20 @@ +belongsTo(User::class); + } +} diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/ProfileFactory.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/ProfileFactory.php new file mode 100644 index 000000000000..0ab93b048914 --- /dev/null +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/ProfileFactory.php @@ -0,0 +1,22 @@ + UserFactory::new(), + ]; + } + + #[\Override] + public function modelName() + { + return Profile::class; + } +} diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/Team.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/Team.php new file mode 100644 index 000000000000..24060ced1eb0 --- /dev/null +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/Team.php @@ -0,0 +1,31 @@ + 'boolean', + ]; + } + + public function users() + { + return $this->belongsToMany(User::class) + ->withPivot('role') + ->withTimestamps() + ->using(Membership::class) + ->as('membership'); + } +} diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/TeamFactory.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/TeamFactory.php new file mode 100644 index 000000000000..0d960f8c6916 --- /dev/null +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/TeamFactory.php @@ -0,0 +1,24 @@ + $this->faker->unique()->company(), + 'user_id' => UserFactory::new(), + 'personal_team' => true, + ]; + } + + #[\Override] + public function modelName() + { + return Team::class; + } +} diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/User.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/User.php new file mode 100644 index 000000000000..ef7cac2097a2 --- /dev/null +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/User.php @@ -0,0 +1,35 @@ +hasOne(Profile::class); + } + + public function posts() + { + return $this->hasMany(Post::class); + } + + public function teams() + { + return $this->belongsToMany(Team::class) + ->withPivot('role') + ->withTimestamps() + ->using(Membership::class) + ->as('membership'); + } +} diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/UserApiResource.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/UserApiResource.php new file mode 100644 index 000000000000..7d1a7c7918c5 --- /dev/null +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/UserApiResource.php @@ -0,0 +1,23 @@ + $this->name, + 'email' => $this->email, + ]; + } +} diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/UserResource.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/UserResource.php new file mode 100644 index 000000000000..7ddd2248a37b --- /dev/null +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/UserResource.php @@ -0,0 +1,17 @@ + $this->name, + 'email' => $this->email, + ]; + } +} diff --git a/tests/Integration/Http/Resources/JsonApi/Fixtures/migrations.php b/tests/Integration/Http/Resources/JsonApi/Fixtures/migrations.php new file mode 100644 index 000000000000..e560a3eb970a --- /dev/null +++ b/tests/Integration/Http/Resources/JsonApi/Fixtures/migrations.php @@ -0,0 +1,36 @@ +id(); + $table->foreignId('user_id')->index(); + $table->string('title'); + $table->text('content'); + $table->timestamps(); +}); + +Schema::create('profiles', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->unique(); + $table->date('date_of_birth')->nullable(); + $table->string('timezone')->nullable(); +}); + +Schema::create('teams', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->index(); + $table->string('name'); + $table->boolean('personal_team'); +}); + +Schema::create('team_user', function (Blueprint $table) { + $table->id(); + $table->foreignId('team_id'); + $table->foreignId('user_id'); + $table->string('role')->nullable(); + $table->timestamps(); + + $table->index(['team_id', 'user_id']); +}); diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index 5442bb255a34..e46488710881 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -6,84 +6,24 @@ use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Pivot; -use Illuminate\Database\Schema\Blueprint; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\Request; -use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\JsonApi\JsonApiResource; -use Illuminate\Support\Facades\Schema; -use Orchestra\Testbench\Attributes\WithConfig; -use Orchestra\Testbench\Attributes\WithMigration; +use Illuminate\Http\Resources\Json\JsonResource; +use Illuminate\Tests\Integration\Http\Resources\JsonApi\Fixtures\User; +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; use Orchestra\Testbench\Factories\UserFactory; -use Orchestra\Testbench\TestCase; -#[WithMigration] -#[WithConfig('auth.providers.users.model', User::class)] class JsonApiResourceTest extends TestCase { - use RefreshDatabase; - - /** {@inheritdoc} */ - #[\Override] - protected function tearDown(): void - { - parent::tearDown(); - - JsonResource::flushState(); - JsonApiResource::flushState(); - } - - /** {@inheritdoc} */ - #[\Override] - protected function defineRoutes($router) - { - $router->get('users/{userId}', function (Request $request, $userId) { - return User::find($userId)->toResource(); - }); - } - - /** {@inheritdoc} */ - protected function afterRefreshingDatabase() - { - Schema::create('posts', function (Blueprint $table) { - $table->id(); - $table->foreignId('user_id')->index(); - $table->string('title'); - $table->text('content'); - $table->timestamps(); - }); - - Schema::create('profiles', function (Blueprint $table) { - $table->id(); - $table->foreignId('user_id')->unique(); - $table->date('date_of_birth')->nullable(); - $table->string('timezone')->nullable(); - }); - - Schema::create('teams', function (Blueprint $table) { - $table->id(); - $table->foreignId('user_id')->index(); - $table->string('name'); - $table->boolean('personal_team'); - }); - - Schema::create('team_user', function (Blueprint $table) { - $table->id(); - $table->foreignId('team_id'); - $table->foreignId('user_id'); - $table->string('role')->nullable(); - $table->timestamps(); - - $table->index(['team_id', 'user_id']); - }); - } - public function testItCanGenerateJsonApiResponse() { $user = UserFactory::new()->create(); - $this->getJson('/users/'.$user->getKey()) + $this->getJson("/users/{$user->getKey()}") ->assertHeader('Content-type', 'application/vnd.api+json') ->assertExactJson([ 'data' => [ @@ -102,7 +42,7 @@ public function testItCanGenerateJsonApiResponseWithSparseFieldsets() { $user = UserFactory::new()->create(); - $this->getJson('/users/'.$user->getKey().'?'.http_build_query(['fields' => ['users' => 'name']])) + $this->getJson("/users/{$user->getKey()}?".http_build_query(['fields' => ['users' => 'name']])) ->assertHeader('Content-type', 'application/vnd.api+json') ->assertExactJson([ 'data' => [ @@ -120,7 +60,7 @@ public function testItCanGenerateJsonApiResponseWithEmptyRelationship() { $user = UserFactory::new()->create(); - $this->getJson('/users/'.$user->getKey().'?'.http_build_query(['includes' => ['posts']])) + $this->getJson("/users/{$user->getKey()}?".http_build_query(['includes' => ['posts']])) ->assertHeader('Content-type', 'application/vnd.api+json') ->assertExactJson([ 'data' => [ @@ -138,21 +78,21 @@ public function testItCanGenerateJsonApiResponseWithEmptyRelationship() public function testItCanGenerateJsonApiResponseWithEagerLoadedRelationship() { $now = $this->freezeSecond(); - $user = UserFactory::new()->create(); - $profile = ProfileFactory::new()->create([ + $user = User::factory()->create(); + $profile = Profile::factory()->create([ 'user_id' => $user->getKey(), 'date_of_birth' => '2011-06-09', 'timezone' => 'America/Chicago', ]); - $team = TeamFactory::new()->create([ + $team = Team::factory()->create([ 'name' => 'Laravel Team', ]); $user->teams()->attach($team, ['role' => 'Admin']); $user->teams()->attach($team, ['role' => 'Member']); - $posts = PostFactory::new()->times(2)->create([ + $posts = Post::factory()->times(2)->create([ 'user_id' => $user->getKey(), ]); @@ -252,146 +192,6 @@ public function testItCanGenerateJsonApiResponseWithEagerLoadedRelationship() } } -#[UseResource(UserApiResource::class)] -class User extends Authenticatable -{ - public function profile() - { - return $this->hasOne(Profile::class); - } - - public function posts() - { - return $this->hasMany(Post::class); - } - - public function teams() - { - return $this->belongsToMany(Team::class) - ->withPivot('role') - ->withTimestamps() - ->using(Membership::class) - ->as('membership'); - } -} - -class UserResource extends JsonResource -{ - public function toArray(Request $request) - { - return [ - 'name' => $this->name, - 'email' => $this->email, - ]; - } -} - -class UserApiResource extends JsonApiResource -{ - protected array $relationships = [ - 'profile', - 'posts', - 'teams', - ]; - - public function toAttributes(Request $request) - { - return [ - 'name' => $this->name, - 'email' => $this->email, - ]; - } -} - -#[UseResource(PostApiResource::class)] -class Post extends Model -{ - public function user() - { - return $this->belongsTo(User::class); - } -} - -class PostApiResource extends JsonApiResource -{ - protected array $attributes = [ - 'title', - 'content', - ]; -} - -class PostFactory extends Factory -{ - public function definition(): array - { - return [ - 'user_id' => UserFactory::new(), - 'title' => $this->faker->word(), - 'content' => $this->faker->words(10, true), - ]; - } - - #[\Override] - public function modelName() - { - return Post::class; - } -} - -class Team extends Model -{ - public $timestamps = false; - - protected function casts(): array - { - return [ - 'personal_team' => 'boolean', - ]; - } - - public function users() - { - return $this->belongsToMany(User::class) - ->withPivot('role') - ->withTimestamps() - ->using(Membership::class) - ->as('membership'); - } -} - -class Membership extends Pivot -{ - protected $table = 'team_user'; -} - -class TeamFactory extends Factory -{ - public function definition(): array - { - return [ - 'name' => $this->faker->unique()->company(), - 'user_id' => UserFactory::new(), - 'personal_team' => true, - ]; - } - - #[\Override] - public function modelName() - { - return Team::class; - } -} - -class Profile extends Model -{ - public $timestamps = false; - - public function user() - { - return $this->belongsTo(User::class); - } -} - class ProfileFactory extends Factory { public function definition(): array diff --git a/tests/Integration/Http/Resources/JsonApi/TestCase.php b/tests/Integration/Http/Resources/JsonApi/TestCase.php new file mode 100644 index 000000000000..2b06938824cf --- /dev/null +++ b/tests/Integration/Http/Resources/JsonApi/TestCase.php @@ -0,0 +1,43 @@ +get('users/{userId}', function (Request $request, $userId) { + return User::find($userId)->toResource(); + }); + } + + /** {@inheritdoc} */ + protected function afterRefreshingDatabase() + { + require __DIR__.'/Fixtures/migrations.php'; + } +} From 51f5f9f274035cc4261b0e4a4d3d672d805ec54f Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 6 Nov 2025 06:01:25 +0000 Subject: [PATCH 21/34] Apply fixes from StyleCI --- .../Http/Resources/JsonApi/JsonApiResourceTest.php | 10 +--------- tests/Integration/Http/Resources/JsonApi/TestCase.php | 2 +- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index e46488710881..cef087cf64cc 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -2,19 +2,11 @@ namespace Illuminate\Tests\Integration\Http\Resources\JsonApi; -use Illuminate\Database\Eloquent\Attributes\UseResource; use Illuminate\Database\Eloquent\Factories\Factory; -use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\Pivot; -use Illuminate\Foundation\Auth\User as Authenticatable; -use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Http\Request; -use Illuminate\Http\Resources\JsonApi\JsonApiResource; -use Illuminate\Http\Resources\Json\JsonResource; -use Illuminate\Tests\Integration\Http\Resources\JsonApi\Fixtures\User; 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; +use Illuminate\Tests\Integration\Http\Resources\JsonApi\Fixtures\User; use Orchestra\Testbench\Factories\UserFactory; class JsonApiResourceTest extends TestCase diff --git a/tests/Integration/Http/Resources/JsonApi/TestCase.php b/tests/Integration/Http/Resources/JsonApi/TestCase.php index 2b06938824cf..9e618122a3bb 100644 --- a/tests/Integration/Http/Resources/JsonApi/TestCase.php +++ b/tests/Integration/Http/Resources/JsonApi/TestCase.php @@ -4,8 +4,8 @@ use Illuminate\Foundation\Testing\LazilyRefreshDatabase; use Illuminate\Http\Request; -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\User; use Orchestra\Testbench\Attributes\WithConfig; use Orchestra\Testbench\Attributes\WithMigration; From 227ef2afec491b9de5d546fdad63a13031739bd1 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 6 Nov 2025 14:01:55 +0800 Subject: [PATCH 22/34] wip Signed-off-by: Mior Muhammad Zaki --- .../Resources/JsonApi/JsonApiResourceTest.php | 22 +++---------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index cef087cf64cc..83752f9b570a 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -13,7 +13,7 @@ class JsonApiResourceTest extends TestCase { public function testItCanGenerateJsonApiResponse() { - $user = UserFactory::new()->create(); + $user = User::factory()->create(); $this->getJson("/users/{$user->getKey()}") ->assertHeader('Content-type', 'application/vnd.api+json') @@ -32,7 +32,7 @@ public function testItCanGenerateJsonApiResponse() public function testItCanGenerateJsonApiResponseWithSparseFieldsets() { - $user = UserFactory::new()->create(); + $user = User::factory()->create(); $this->getJson("/users/{$user->getKey()}?".http_build_query(['fields' => ['users' => 'name']])) ->assertHeader('Content-type', 'application/vnd.api+json') @@ -50,7 +50,7 @@ public function testItCanGenerateJsonApiResponseWithSparseFieldsets() public function testItCanGenerateJsonApiResponseWithEmptyRelationship() { - $user = UserFactory::new()->create(); + $user = User::factory()->create(); $this->getJson("/users/{$user->getKey()}?".http_build_query(['includes' => ['posts']])) ->assertHeader('Content-type', 'application/vnd.api+json') @@ -183,19 +183,3 @@ public function testItCanGenerateJsonApiResponseWithEagerLoadedRelationship() ]); } } - -class ProfileFactory extends Factory -{ - public function definition(): array - { - return [ - 'user_id' => UserFactory::new(), - ]; - } - - #[\Override] - public function modelName() - { - return Profile::class; - } -} From a7ec8d090202e20de67d344832e3f44ec2cfaba7 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 6 Nov 2025 06:03:19 +0000 Subject: [PATCH 23/34] Apply fixes from StyleCI --- .../Integration/Http/Resources/JsonApi/JsonApiResourceTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index 83752f9b570a..5f86638b0b06 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -2,12 +2,10 @@ namespace Illuminate\Tests\Integration\Http\Resources\JsonApi; -use Illuminate\Database\Eloquent\Factories\Factory; 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; use Illuminate\Tests\Integration\Http\Resources\JsonApi\Fixtures\User; -use Orchestra\Testbench\Factories\UserFactory; class JsonApiResourceTest extends TestCase { From a949f45e5bc8aa64d156bdc42e7dd5673c9ac56a Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 6 Nov 2025 14:13:39 +0800 Subject: [PATCH 24/34] wip Signed-off-by: Mior Muhammad Zaki --- .../JsonApi/Concerns/ResolvesJsonApiElements.php | 9 +++++---- .../Resources/JsonApi/JsonApiResourceTest.php | 15 +++++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index a024dc1c0fab..1ff61d96877b 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -8,10 +8,11 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Database\Eloquent\Relations\Relation; -use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\JsonApi\Exceptions\ResourceIdentificationException; use Illuminate\Http\Resources\JsonApi\JsonApiRequest; use Illuminate\Http\Resources\JsonApi\JsonApiResource; +use Illuminate\Http\Resources\Json\JsonResource; +use Illuminate\Http\Resources\MissingValue; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Str; @@ -133,7 +134,7 @@ protected function resolveResourceRelationshipIdentifiers(JsonApiRequest $reques $this->compileResourceRelationships($request); return [ - ...$this->loadedRelationshipIdentifiers, + ...$this->filter($this->loadedRelationshipIdentifiers), ]; } @@ -190,7 +191,7 @@ protected function compileResourceRelationships(JsonApiRequest $request): void if (is_null($relatedModel) || $relatedModel instanceof Pivot || in_array(AsPivot::class, class_uses_recursive($relatedModel), true)) { - return [$key => null]; + return [$key => new MissingValue]; } return [$key => ['data' => [transform( @@ -201,7 +202,7 @@ function ($uniqueKey) use ($relatedModel) { return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]]; } )]]]; - })->filter()->all(); + })->all(); } /** diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index 5f86638b0b06..1ffd838c7b35 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -46,11 +46,11 @@ public function testItCanGenerateJsonApiResponseWithSparseFieldsets() ->assertJsonMissing(['jsonapi', 'included']); } - public function testItCanGenerateJsonApiResponseWithEmptyRelationship() + public function testItCanGenerateJsonApiResponseWithEmptyRelationshipsUsingSparseIncluded() { $user = User::factory()->create(); - $this->getJson("/users/{$user->getKey()}?".http_build_query(['includes' => ['posts']])) + $this->getJson("/users/{$user->getKey()}?".http_build_query(['include' => 'posts'])) ->assertHeader('Content-type', 'application/vnd.api+json') ->assertExactJson([ 'data' => [ @@ -60,15 +60,22 @@ public function testItCanGenerateJsonApiResponseWithEmptyRelationship() 'name' => $user->name, 'email' => $user->email, ], + 'relationships' => [ + 'posts' => [ + 'data' => [], + ], + ], ], ]) ->assertJsonMissing(['jsonapi', 'included']); } - public function testItCanGenerateJsonApiResponseWithEagerLoadedRelationship() + public function testItCanGenerateJsonApiResponseWithRelationshipsUsingSparseIncluded() { $now = $this->freezeSecond(); + $user = User::factory()->create(); + $profile = Profile::factory()->create([ 'user_id' => $user->getKey(), 'date_of_birth' => '2011-06-09', @@ -86,7 +93,7 @@ public function testItCanGenerateJsonApiResponseWithEagerLoadedRelationship() 'user_id' => $user->getKey(), ]); - $this->getJson('/users/'.$user->getKey().'?'.http_build_query(['include' => 'profile,posts,teams'])) + $this->getJson("/users/{$user->getKey()}?".http_build_query(['include' => 'profile,posts,teams'])) ->assertHeader('Content-type', 'application/vnd.api+json') ->assertExactJson([ 'data' => [ From 1fcd9a7cd3ecc6ccd5f3d3c6b0382226afeee3fe Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 6 Nov 2025 06:13:53 +0000 Subject: [PATCH 25/34] Apply fixes from StyleCI --- .../Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 1ff61d96877b..e56317fe4610 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -8,10 +8,10 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Database\Eloquent\Relations\Relation; +use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\JsonApi\Exceptions\ResourceIdentificationException; use Illuminate\Http\Resources\JsonApi\JsonApiRequest; use Illuminate\Http\Resources\JsonApi\JsonApiResource; -use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\MissingValue; use Illuminate\Support\Arr; use Illuminate\Support\Collection; From 3cde25300cacbed3ecab892749b4d8d76f27a480 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 6 Nov 2025 15:03:15 +0800 Subject: [PATCH 26/34] Add `make:resource` with `--json-api` option Signed-off-by: Mior Muhammad Zaki --- .../Console/ResourceMakeCommand.php | 9 ++++++--- .../Console/stubs/resource-json-api.stub | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 src/Illuminate/Foundation/Console/stubs/resource-json-api.stub diff --git a/src/Illuminate/Foundation/Console/ResourceMakeCommand.php b/src/Illuminate/Foundation/Console/ResourceMakeCommand.php index 51d96120cc00..2b6d8927204e 100644 --- a/src/Illuminate/Foundation/Console/ResourceMakeCommand.php +++ b/src/Illuminate/Foundation/Console/ResourceMakeCommand.php @@ -51,9 +51,11 @@ public function handle() */ protected function getStub() { - return $this->collection() - ? $this->resolveStubPath('/stubs/resource-collection.stub') - : $this->resolveStubPath('/stubs/resource.stub'); + return match (true) { + $this->collection() => $this->resolveStubPath('/stubs/resource-collection.stub'), + $this->option('json-api') => $this->resolveStubPath('/stubs/resource-json-api.stub'), + default => $this->resolveStubPath('/stubs/resource.stub'), + }; } /** @@ -101,6 +103,7 @@ protected function getOptions() return [ ['force', 'f', InputOption::VALUE_NONE, 'Create the class even if the resource already exists'], ['collection', 'c', InputOption::VALUE_NONE, 'Create a resource collection'], + ['json-api', 'j', InputOption::VALUE_NONE, 'Create a JSON:API resource'], ]; } } diff --git a/src/Illuminate/Foundation/Console/stubs/resource-json-api.stub b/src/Illuminate/Foundation/Console/stubs/resource-json-api.stub new file mode 100644 index 000000000000..7c5f19ca6bb2 --- /dev/null +++ b/src/Illuminate/Foundation/Console/stubs/resource-json-api.stub @@ -0,0 +1,19 @@ + + */ + public function toAttributes(Request $request): array + { + return parent::toArray($request); + } +} From a9d041dea56b806dbcfeeed8d3b2ee1bafd25660 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 6 Nov 2025 15:14:49 +0800 Subject: [PATCH 27/34] Apply suggestions from code review --- .../Foundation/Console/stubs/resource-json-api.stub | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Foundation/Console/stubs/resource-json-api.stub b/src/Illuminate/Foundation/Console/stubs/resource-json-api.stub index 7c5f19ca6bb2..ba554d366b58 100644 --- a/src/Illuminate/Foundation/Console/stubs/resource-json-api.stub +++ b/src/Illuminate/Foundation/Console/stubs/resource-json-api.stub @@ -10,10 +10,10 @@ class {{ class }} extends JsonApiResource /** * Transform the resource into an array. * - * @return array + * @return list|array */ public function toAttributes(Request $request): array { - return parent::toArray($request); + return parent::toAttributes($request); } } From f59070b6343b2b1c851eddf379fdc95d2a55c6cd Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Fri, 7 Nov 2025 12:03:06 +0800 Subject: [PATCH 28/34] ensure request is always `JsonApiRequest` --- .../Http/Resources/JsonApi/AnonymousResourceCollection.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php index d6e3c1a50b0c..26782752de37 100644 --- a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php +++ b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php @@ -19,6 +19,8 @@ class AnonymousResourceCollection extends \Illuminate\Http\Resources\Json\Anonym #[\Override] public function with($request) { + $request = $this->resolveJsonApiRequestFrom($request); + return array_filter([ 'included' => $this->collection ->map(fn ($resource) => $resource->resolveIncludedResources($request)) @@ -39,6 +41,8 @@ public function with($request) #[\Override] public function toAttributes(Request $request) { + $request = $this->resolveJsonApiRequestFrom($request); + return $this->collection ->map(fn ($resource) => $resource->resolveResourceData($request)) ->all(); From d320641545e40871e1663d08a76226063ef8413a Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Fri, 7 Nov 2025 14:07:03 +0800 Subject: [PATCH 29/34] wip Signed-off-by: Mior Muhammad Zaki --- .../JsonApi/AnonymousResourceCollection.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php index 26782752de37..08247507ca36 100644 --- a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php +++ b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php @@ -19,8 +19,6 @@ class AnonymousResourceCollection extends \Illuminate\Http\Resources\Json\Anonym #[\Override] public function with($request) { - $request = $this->resolveJsonApiRequestFrom($request); - return array_filter([ 'included' => $this->collection ->map(fn ($resource) => $resource->resolveIncludedResources($request)) @@ -41,8 +39,6 @@ public function with($request) #[\Override] public function toAttributes(Request $request) { - $request = $this->resolveJsonApiRequestFrom($request); - return $this->collection ->map(fn ($resource) => $resource->resolveResourceData($request)) ->all(); @@ -61,6 +57,18 @@ public function withResponse(Request $request, JsonResponse $response): void $response->header('Content-Type', 'application/vnd.api+json'); } + /** + * Create an HTTP response that represents the object. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + #[\Override] + public function toResponse($request) + { + return parent::toResponse($this->resolveJsonApiRequestFrom($request)); + } + /** * Resolve the HTTP request instance from container. * From 4ae7eb725d67a2b528afabdc97b5713e7ee71e38 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Fri, 7 Nov 2025 14:12:22 +0800 Subject: [PATCH 30/34] wip Signed-off-by: Mior Muhammad Zaki --- .../Http/Resources/JsonApi/JsonApiRequestTest.php | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/Integration/Http/Resources/JsonApi/JsonApiRequestTest.php diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiRequestTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiRequestTest.php new file mode 100644 index 000000000000..2c7332952d91 --- /dev/null +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiRequestTest.php @@ -0,0 +1,11 @@ + Date: Fri, 7 Nov 2025 06:12:47 +0000 Subject: [PATCH 31/34] Apply fixes from StyleCI --- tests/Integration/Http/Resources/JsonApi/JsonApiRequestTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiRequestTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiRequestTest.php index 2c7332952d91..cb8fbbd294ef 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiRequestTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiRequestTest.php @@ -6,6 +6,5 @@ class JsonApiRequestTest extends TestCase { public function testItCanResolveSparseFieldset(string $queryString, array $expected) { - } } From 64cacc96efa149b3ea27599ec78520047bc32392 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Fri, 7 Nov 2025 18:14:03 +0800 Subject: [PATCH 32/34] wip Signed-off-by: Mior Muhammad Zaki --- .../Resources/JsonApi/JsonApiRequestTest.php | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiRequestTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiRequestTest.php index cb8fbbd294ef..39af5098153a 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiRequestTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiRequestTest.php @@ -2,9 +2,46 @@ namespace Illuminate\Tests\Integration\Http\Resources\JsonApi; +use Illuminate\Http\Resources\JsonApi\JsonApiRequest; + class JsonApiRequestTest extends TestCase { - public function testItCanResolveSparseFieldset(string $queryString, array $expected) + public function testItCanResolveSparseFields() + { + $request = JsonApiRequest::create(uri: '/?'.http_build_query([ + 'fields' => [ + 'users' => 'name,email', + 'teams' => 'name', + ], + ])); + + $this->assertSame(['name', 'email'], $request->sparseFields('users')); + $this->assertSame(['name'], $request->sparseFields('teams')); + $this->assertSame([], $request->sparseFields('posts')); + } + + public function testItCanResolveEmptySparseFields() + { + $request = JsonApiRequest::create(uri: '/'); + + $this->assertSame([], $request->sparseFields('users')); + $this->assertSame([], $request->sparseFields('teams')); + $this->assertSame([], $request->sparseFields('posts')); + } + + public function testItCanResolveSparseIncluded() { + $request = JsonApiRequest::create(uri: '/?'.http_build_query([ + 'include' => 'teams,users', + ])); + + $this->assertSame(['teams', 'users'], $request->sparseIncluded()); + } + + public function testItCanResolveEmptySparseIncluded() + { + $request = JsonApiRequest::create(uri: '/'); + + $this->assertSame([], $request->sparseIncluded()); } } From 5748a9f332f4ee7b7861d4f225a28ec581e0d010 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Fri, 7 Nov 2025 19:00:54 +0800 Subject: [PATCH 33/34] wip Signed-off-by: Mior Muhammad Zaki --- .../Concerns/ResolvesJsonApiElements.php | 10 +- .../JsonApi/JsonApiCollectionTest.php | 209 ++++++++++++++++++ .../Resources/JsonApi/JsonApiResourceTest.php | 22 +- .../Http/Resources/JsonApi/TestCase.php | 6 +- 4 files changed, 232 insertions(+), 15 deletions(-) create mode 100644 tests/Integration/Http/Resources/JsonApi/JsonApiCollectionTest.php diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index e56317fe4610..2b2a4ef4e5da 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -134,7 +134,10 @@ protected function resolveResourceRelationshipIdentifiers(JsonApiRequest $reques $this->compileResourceRelationships($request); return [ - ...$this->filter($this->loadedRelationshipIdentifiers), + ...(new Collection($this->filter($this->loadedRelationshipIdentifiers))) + ->map(function ($relation) { + return ! is_null($relation) ? $relation : ['data' => []]; + })->all(), ]; } @@ -188,8 +191,9 @@ protected function compileResourceRelationships(JsonApiRequest $request): void // Relationship is a single model... $relatedModel = $relatedModels; - if (is_null($relatedModel) || - $relatedModel instanceof Pivot || + if (is_null($relatedModel)) { + return [$key => null]; + } elseif ($relatedModel instanceof Pivot || in_array(AsPivot::class, class_uses_recursive($relatedModel), true)) { return [$key => new MissingValue]; } diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiCollectionTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiCollectionTest.php new file mode 100644 index 000000000000..d3d8ada6bae7 --- /dev/null +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiCollectionTest.php @@ -0,0 +1,209 @@ +times(5)->create(); + + $this->getJson('/users') + ->assertHeader('Content-type', 'application/vnd.api+json') + ->assertJsonPath( + 'data', + $users->transform(fn ($user) => [ + 'id' => (string) $user->getKey(), + 'type' => 'users', + 'attributes' => [ + 'name' => $user->name, + 'email' => $user->email, + ], + ])->all() + )->assertJsonMissing(['jsonapi', 'included']); + } + + public function testItCanGenerateJsonApiResponseWithSparseFieldsets() + { + $users = User::factory()->times(5)->create(); + + $this->getJson('/users/?'.http_build_query(['fields' => ['users' => 'name']])) + ->assertHeader('Content-type', 'application/vnd.api+json') + ->assertJsonPath( + 'data', + $users->transform(fn ($user) => [ + 'id' => (string) $user->getKey(), + 'type' => 'users', + 'attributes' => [ + 'name' => $user->name, + ], + ])->all() + )->assertJsonMissing(['jsonapi', 'included']); + } + + public function testItCanGenerateJsonApiResponseWithEmptyRelationshipsUsingSparseIncluded() + { + $users = User::factory()->times(5)->create(); + + $this->getJson('/users/?'.http_build_query(['include' => 'posts'])) + ->assertHeader('Content-type', 'application/vnd.api+json') + ->assertJsonPath( + 'data', + $users->transform(fn ($user) => [ + 'id' => (string) $user->getKey(), + 'type' => 'users', + 'attributes' => [ + 'name' => $user->name, + 'email' => $user->email, + ], + 'relationships' => [ + 'posts' => [ + 'data' => [], + ], + ], + ])->all() + )->assertJsonMissing(['jsonapi', 'included']); + } + + public function testItCanGenerateJsonApiResponseWithRelationshipsUsingSparseIncluded() + { + $now = $this->freezeSecond(); + + $users = User::factory()->times(4)->create(); + $user = User::factory()->create(); + + $profile = Profile::factory()->create([ + 'user_id' => $user->getKey(), + 'date_of_birth' => '2011-06-09', + 'timezone' => 'America/Chicago', + ]); + + $team = Team::factory()->create([ + 'name' => 'Laravel Team', + ]); + + $user->teams()->attach($team, ['role' => 'Admin']); + $user->teams()->attach($team, ['role' => 'Member']); + + $posts = Post::factory()->times(2)->create([ + 'user_id' => $user->getKey(), + ]); + + $this->getJson('/users?'.http_build_query(['include' => 'profile,posts,teams'])) + ->assertHeader('Content-type', 'application/vnd.api+json') + ->assertJsonPath( + 'data', + [ + ...$users->transform(fn ($user) => [ + 'id' => (string) $user->getKey(), + 'type' => 'users', + 'attributes' => [ + 'name' => $user->name, + 'email' => $user->email, + ], + 'relationships' => [ + 'profile' => ['data' => []], + 'posts' => ['data' => []], + 'teams' => ['data' => []], + ], + ])->all(), + [ + 'id' => (string) $user->getKey(), + 'type' => 'users', + 'attributes' => [ + 'name' => $user->name, + 'email' => $user->email, + ], + 'relationships' => [ + 'profile' => [ + 'data' => [ + ['id' => (string) $profile->getKey(), 'type' => 'profiles'], + ], + ], + 'posts' => [ + 'data' => [ + ['id' => (string) $posts[0]->getKey(), 'type' => 'posts'], + ['id' => (string) $posts[1]->getKey(), 'type' => 'posts'], + ], + ], + 'teams' => [ + 'data' => [ + ['id' => (string) $team->getKey(), 'type' => 'teams'], + ['id' => (string) $team->getKey(), 'type' => 'teams'], + ], + ], + ], + ] + ] + )->assertJsonPath( + 'included', + [ + [ + 'id' => (string) $profile->getKey(), + 'type' => 'profiles', + 'attributes' => [ + 'id' => $profile->getKey(), + 'user_id' => $user->getKey(), + 'date_of_birth' => '2011-06-09', + 'timezone' => 'America/Chicago', + ], + ], + [ + 'id' => (string) $posts[0]->getKey(), + 'type' => 'posts', + 'attributes' => [ + 'title' => $posts[0]->title, + 'content' => $posts[0]->content, + ], + ], + [ + 'id' => (string) $posts[1]->getKey(), + 'type' => 'posts', + 'attributes' => [ + 'title' => $posts[1]->title, + 'content' => $posts[1]->content, + ], + ], + [ + 'id' => (string) $team->getKey(), + 'type' => 'teams', + 'attributes' => [ + 'id' => $team->getKey(), + 'user_id' => $team->user_id, + 'name' => 'Laravel Team', + 'personal_team' => true, + 'membership' => [ + 'user_id' => $user->getKey(), + 'team_id' => $team->getKey(), + 'role' => 'Admin', + 'created_at' => $now->toISOString(), + 'updated_at' => $now->toISOString(), + ], + ], + ], + [ + 'id' => (string) $team->getKey(), + 'type' => 'teams', + 'attributes' => [ + 'id' => $team->getKey(), + 'user_id' => $team->user_id, + 'name' => 'Laravel Team', + 'personal_team' => true, + 'membership' => [ + 'user_id' => $user->getKey(), + 'team_id' => $team->getKey(), + 'role' => 'Member', + 'created_at' => $now->toISOString(), + 'updated_at' => $now->toISOString(), + ], + ], + ], + ] + ); + } +} diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index 1ffd838c7b35..fe166c5c10ba 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -131,7 +131,7 @@ public function testItCanGenerateJsonApiResponseWithRelationshipsUsingSparseIncl 'date_of_birth' => '2011-06-09', 'id' => $profile->getKey(), 'timezone' => 'America/Chicago', - 'user_id' => 1, + 'user_id' => $user->getKey(), ], ], [ @@ -155,16 +155,16 @@ public function testItCanGenerateJsonApiResponseWithRelationshipsUsingSparseIncl 'type' => 'teams', 'attributes' => [ 'id' => $team->getKey(), + 'user_id' => $team->user_id, + 'name' => 'Laravel Team', + 'personal_team' => true, 'membership' => [ - 'created_at' => $now, + 'created_at' => $now->toISOString(), 'role' => 'Admin', 'team_id' => $team->getKey(), 'user_id' => $user->getKey(), - 'updated_at' => $now, + 'updated_at' => $now->toISOString(), ], - 'name' => 'Laravel Team', - 'personal_team' => true, - 'user_id' => $team->user_id, ], ], [ @@ -172,16 +172,16 @@ public function testItCanGenerateJsonApiResponseWithRelationshipsUsingSparseIncl 'type' => 'teams', 'attributes' => [ 'id' => $team->getKey(), + 'user_id' => $team->user_id, + 'name' => 'Laravel Team', + 'personal_team' => true, 'membership' => [ - 'created_at' => $now, + 'created_at' => $now->toISOString(), 'role' => 'Member', 'team_id' => $team->getKey(), 'user_id' => $user->getKey(), - 'updated_at' => $now, + 'updated_at' => $now->toISOString(), ], - 'name' => 'Laravel Team', - 'personal_team' => true, - 'user_id' => $team->user_id, ], ], ], diff --git a/tests/Integration/Http/Resources/JsonApi/TestCase.php b/tests/Integration/Http/Resources/JsonApi/TestCase.php index 9e618122a3bb..df70b5d2cc2b 100644 --- a/tests/Integration/Http/Resources/JsonApi/TestCase.php +++ b/tests/Integration/Http/Resources/JsonApi/TestCase.php @@ -30,7 +30,11 @@ protected function tearDown(): void #[\Override] protected function defineRoutes($router) { - $router->get('users/{userId}', function (Request $request, $userId) { + $router->get('users', function () { + return User::paginate(5)->toResourceCollection(); + }); + + $router->get('users/{userId}', function ($userId) { return User::find($userId)->toResource(); }); } From acbde383a1576f4caa171d77cdaf1c45ad72b2e4 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Fri, 7 Nov 2025 11:01:08 +0000 Subject: [PATCH 34/34] Apply fixes from StyleCI --- .../Http/Resources/JsonApi/JsonApiCollectionTest.php | 2 +- tests/Integration/Http/Resources/JsonApi/TestCase.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiCollectionTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiCollectionTest.php index d3d8ada6bae7..b295eddd36cb 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiCollectionTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiCollectionTest.php @@ -138,7 +138,7 @@ public function testItCanGenerateJsonApiResponseWithRelationshipsUsingSparseIncl ], ], ], - ] + ], ] )->assertJsonPath( 'included', diff --git a/tests/Integration/Http/Resources/JsonApi/TestCase.php b/tests/Integration/Http/Resources/JsonApi/TestCase.php index df70b5d2cc2b..a1644383a7cb 100644 --- a/tests/Integration/Http/Resources/JsonApi/TestCase.php +++ b/tests/Integration/Http/Resources/JsonApi/TestCase.php @@ -3,7 +3,6 @@ namespace Illuminate\Tests\Integration\Http\Resources\JsonApi; use Illuminate\Foundation\Testing\LazilyRefreshDatabase; -use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\JsonApi\JsonApiResource; use Illuminate\Tests\Integration\Http\Resources\JsonApi\Fixtures\User;