From fce2a2e91b858a848409fd7b15996e3029253d76 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 23 Oct 2025 09:14:24 +0800 Subject: [PATCH 01/74] wip Signed-off-by: Mior Muhammad Zaki --- .../ResourceIdentificationException.php | 36 +++++++++++++++ .../UnknownRelationshipException.php | 26 +++++++++++ .../Resources/JsonApi/JsonApiResource.php | 46 +++++++++++++++++++ .../JsonApi/JsonApiResourceCollection.php | 8 ++++ .../Http/Resources/JsonApi/Types/Link.php | 8 ++++ 5 files changed, 124 insertions(+) create mode 100644 src/Illuminate/Http/Resources/JsonApi/Exceptions/ResourceIdentificationException.php create mode 100644 src/Illuminate/Http/Resources/JsonApi/Exceptions/UnknownRelationshipException.php create mode 100644 src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php create mode 100644 src/Illuminate/Http/Resources/JsonApi/JsonApiResourceCollection.php create mode 100644 src/Illuminate/Http/Resources/JsonApi/Types/Link.php diff --git a/src/Illuminate/Http/Resources/JsonApi/Exceptions/ResourceIdentificationException.php b/src/Illuminate/Http/Resources/JsonApi/Exceptions/ResourceIdentificationException.php new file mode 100644 index 000000000000..0a25a5f0eed9 --- /dev/null +++ b/src/Illuminate/Http/Resources/JsonApi/Exceptions/ResourceIdentificationException.php @@ -0,0 +1,36 @@ + $this->resolveId($request), + 'type' => $this->resolveType($request), + ...(new Collection([ + 'attributes' => $this->requestedAttributes($request)->all(), + 'relationships' => $this->requestedRelationshipsAsIdentifiers($request)->all(), + 'links' => self::parseLinks(array_merge($this->toLinks($request), $this->links)), + 'meta' => array_merge($this->toMeta($request), $this->meta), + ]))->filter()->map(fn ($value) => (object) $value), + ]; + } + + + /** + * Create a new resource collection instance. + * + * @param mixed $resource + * @return \Illuminate\Http\Resources\JsonApi\JsonApiResourceCollection + */ + #[\Override] + protected static function newCollection($resource) + { + return new JsonApiResourceCollection($resource, static::class); + } + +} diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResourceCollection.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResourceCollection.php new file mode 100644 index 000000000000..47ce5719eaa5 --- /dev/null +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResourceCollection.php @@ -0,0 +1,8 @@ + Date: Tue, 28 Oct 2025 19:31:25 +0800 Subject: [PATCH 02/74] wip Signed-off-by: Mior Muhammad Zaki --- .../Json/AnonymousJsonApiResource.php | 32 ++++ .../InteractsWithMetaInformations.php | 19 +++ .../Http/Resources/Json/JsonApiResource.php | 139 +++++++++++++++++ .../Json/JsonApiResourceCollection.php | 147 ++++++++++++++++++ .../Http/Resources/Json/JsonResource.php | 11 ++ .../Resources/JsonApi/JsonApiResource.php | 46 ------ .../JsonApi/JsonApiResourceCollection.php | 8 - .../Http/Resources/JsonApi/Types/Link.php | 46 +++++- 8 files changed, 393 insertions(+), 55 deletions(-) create mode 100644 src/Illuminate/Http/Resources/Json/AnonymousJsonApiResource.php create mode 100644 src/Illuminate/Http/Resources/Json/Concerns/InteractsWithMetaInformations.php create mode 100644 src/Illuminate/Http/Resources/Json/JsonApiResource.php create mode 100644 src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php delete mode 100644 src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php delete mode 100644 src/Illuminate/Http/Resources/JsonApi/JsonApiResourceCollection.php diff --git a/src/Illuminate/Http/Resources/Json/AnonymousJsonApiResource.php b/src/Illuminate/Http/Resources/Json/AnonymousJsonApiResource.php new file mode 100644 index 000000000000..3ad1979d2a3d --- /dev/null +++ b/src/Illuminate/Http/Resources/Json/AnonymousJsonApiResource.php @@ -0,0 +1,32 @@ +version ?? static::$jsonApiVersion; + } + + /** + * Set the JSON:API version for the request. + * + * @param string $version + * @return $this + */ + public function withVersion(string $version) + { + $this->version = $version; + + return $this; + } +} diff --git a/src/Illuminate/Http/Resources/Json/Concerns/InteractsWithMetaInformations.php b/src/Illuminate/Http/Resources/Json/Concerns/InteractsWithMetaInformations.php new file mode 100644 index 000000000000..e1e81ef74e4a --- /dev/null +++ b/src/Illuminate/Http/Resources/Json/Concerns/InteractsWithMetaInformations.php @@ -0,0 +1,19 @@ + + */ + public function resolveMetaInformations(Request $request): array + { + return array_merge($this->meta($request), $this->meta); + } +} diff --git a/src/Illuminate/Http/Resources/Json/JsonApiResource.php b/src/Illuminate/Http/Resources/Json/JsonApiResource.php new file mode 100644 index 000000000000..1eabff4ea7b3 --- /dev/null +++ b/src/Illuminate/Http/Resources/Json/JsonApiResource.php @@ -0,0 +1,139 @@ +resource instanceof Model) { + return $this->resource->getKey(); + } + + throw new RuntimeException('Unable to determine "id"'); + } + + public function type(Request $request) + { + if ($this->resource instanceof Model) { + return Str::snake(class_basename($this->resource)); + } + + throw new RuntimeException('Unable to determine "type"'); + } + + public function links(Request $request) + { + return [ + // + ]; + } + + /** + * Set the JSON:API version for the request. + * + * @param string $version + * @return $this + */ + public static function configure(string $version = null, array $ext = [], array $profile = [], array $meta = []) + { + static::$jsonApiInformation = array_filter([ + 'version' => $version, + 'ext' => $ext, + 'profile' => $profile, + 'meta' => $meta, + ]); + + return $this; + } + + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array{id: string, type: string, attributes?: stdClass, relationships?: stdClass, meta?: stdClass, links?: stdClass} + */ + #[\Override] + public function toArray(Request $request) + { + return [ + 'id' => $this->id($request), + 'type' => $this->type($request), + ...(new Collection([ + // 'attributes' => $this->resolveAttributes($request)->all(), + // 'relationships' => $this->resolveRelationshipsAsIdentifiers($request)->all(), + // 'links' => self::parseLinks(array_merge($this->toLinks($request), $this->links)), + // 'meta' => $this->resolveMetaInformations($request), + ]))->filter()->map(fn ($value) => (object) $value), + ]; + } + + /** + * Customize the outgoing response for the resource. + */ + #[\Override] + public function withResponse(Request $request, JsonResponse $response): void + { + $response->header('Content-type', 'application/vnd.api+json'); + } + + /** + * Create a new resource collection instance. + * + * @param mixed $resource + * @return \Illuminate\Http\Resources\JsonApi\JsonApiResourceCollection + */ + #[\Override] + protected static function newCollection($resource) + { + return new JsonApiResourceCollection($resource, static::class); + } + + /** + * Flush the resource's global state. + * + * @return void + */ + public static function flushState() + { + parent::flushState(); + + static::$jsonApiInformation = []; + } +} diff --git a/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php b/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php new file mode 100644 index 000000000000..5b7873b400a7 --- /dev/null +++ b/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php @@ -0,0 +1,147 @@ +collection = $this->collection->map($callback); + + return $this; + } + + /** + * @return RelationshipObject + */ + public function toResourceLink(Request $request) + { + return RelationshipObject::toMany($this->resolveResourceIdentifiers($request)->all()); + } + + /** + * @return Collection + */ + private function resolveResourceIdentifiers(Request $request) + { + return $this->collection + ->uniqueStrict(fn (JsonApiResource $resource): array => $resource->uniqueKey($request)) + ->map(fn (JsonApiResource $resource): ResourceIdentifier => $resource->resolveResourceIdentifier($request)); + } + + /** + * @param \Illuminate\Http\Request $request + * @return array{included?: array, jsonapi?: ServerImplementation} + */ + public function with($request) + { + return [ + ...($included = $this->collection + ->map(fn (JsonApiResource $resource): Collection => $resource->included($request)) + ->flatten() + ->uniqueStrict(fn (JsonApiResource $resource): array => $resource->uniqueKey($request)) + ->values() + ->all()) ? ['included' => $included] : [], + ...($implementation = $this->collects::toServerImplementation($request)) // @TODO + ? ['jsonapi' => $implementation] : [], + ]; + } + + /** + * Create an HTTP response that represents the object. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + #[\Override] + public function toResponse($request) + { + return tap(parent::toResponse($request)->header('Content-type', 'application/vnd.api+json'), $this->flush(...)); + } + + /** + * @param \Illuminate\Http\Request $request + * @param array $paginated + * @param array{links: array} $default + * @return array{links: array} + */ + public function paginationInformation(Request $request, array $paginated, array $default) + { + if (isset($default['links'])) { + $default['links'] = array_filter($default['links'], fn (?string $link): bool => $link !== null); + } + + if (isset($default['meta']['links'])) { + $default['meta']['links'] = array_map( + function (array $link): array { + $link['label'] = (string) $link['label']; + + return $link; + }, + $default['meta']['links'] + ); + } + + return $default; + } + + /** + * Set include prefix to resources. + * + * @internal + * + * @param string $prefix + * @return $this + */ + public function withIncludePrefix(string $prefix) + { + $this->collection->each(fn (JsonApiResource $resource): JsonApiResource => $resource->withIncludePrefix($prefix)); + + return $this; + } + + /** + * Get included resources. + * + * @internal + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Support\Collection> + */ + public function included(Request $request) + { + return $this->collection->map(fn (JsonApiResource $resource): Collection => $resource->included($request)); + } + + /** + * Get the includable collection. + * + * @internal + * + * @return \Illuminate\Support\Collection + */ + public function includable() + { + return $this->collection; + } + + /** + * Flush resource collection states. + * + * @internal + * + * @return void + */ + public function flush(): void + { + $this->collection->each(fn (JsonApiResource $resource) => $resource->flush()); + } +} diff --git a/src/Illuminate/Http/Resources/Json/JsonResource.php b/src/Illuminate/Http/Resources/Json/JsonResource.php index f1d559721e33..f98f2cb53e45 100644 --- a/src/Illuminate/Http/Resources/Json/JsonResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonResource.php @@ -273,4 +273,15 @@ public function jsonSerialize(): array { return $this->resolve(Container::getInstance()->make('request')); } + + /** + * Flush the resource's global state. + * + * @return void + */ + public static function flushState() + { + static::$wrap = 'data'; + static::$forceWrapping = false; + } } diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php deleted file mode 100644 index 0221ed32caa8..000000000000 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ /dev/null @@ -1,46 +0,0 @@ - $this->resolveId($request), - 'type' => $this->resolveType($request), - ...(new Collection([ - 'attributes' => $this->requestedAttributes($request)->all(), - 'relationships' => $this->requestedRelationshipsAsIdentifiers($request)->all(), - 'links' => self::parseLinks(array_merge($this->toLinks($request), $this->links)), - 'meta' => array_merge($this->toMeta($request), $this->meta), - ]))->filter()->map(fn ($value) => (object) $value), - ]; - } - - - /** - * Create a new resource collection instance. - * - * @param mixed $resource - * @return \Illuminate\Http\Resources\JsonApi\JsonApiResourceCollection - */ - #[\Override] - protected static function newCollection($resource) - { - return new JsonApiResourceCollection($resource, static::class); - } - -} diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResourceCollection.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResourceCollection.php deleted file mode 100644 index 47ce5719eaa5..000000000000 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResourceCollection.php +++ /dev/null @@ -1,8 +0,0 @@ - $meta + * @return static + */ + public static function to(string $href, array $meta = []) + { + return new static('self', $href, $meta); + } + + /** + * @param array $meta + * @return static + */ + public static function related(string $href, array $meta = []) + { + return new static('related', $href, $meta); + } + /** + * Prepare the link for JSON serialization. + * + * @return array{href: string, meta?: object} + */ + public function jsonSerialize(): array + { + return [ + 'href' => $this->href, + ...$this->meta ? ['meta' => (object) $this->meta] : [], + ]; + } } From 6457e725fd05a094f363ef0b29bea4084130fe09 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Tue, 28 Oct 2025 11:32:12 +0000 Subject: [PATCH 03/74] Apply fixes from StyleCI --- src/Illuminate/Http/Resources/Json/JsonApiResource.php | 2 +- .../Http/Resources/Json/JsonApiResourceCollection.php | 1 - .../JsonApi/Exceptions/ResourceIdentificationException.php | 2 +- src/Illuminate/Http/Resources/JsonApi/Types/Link.php | 3 ++- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Http/Resources/Json/JsonApiResource.php b/src/Illuminate/Http/Resources/Json/JsonApiResource.php index 1eabff4ea7b3..f57bf1eb5835 100644 --- a/src/Illuminate/Http/Resources/Json/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonApiResource.php @@ -71,7 +71,7 @@ public function links(Request $request) * @param string $version * @return $this */ - public static function configure(string $version = null, array $ext = [], array $profile = [], array $meta = []) + public static function configure(?string $version = null, array $ext = [], array $profile = [], array $meta = []) { static::$jsonApiInformation = array_filter([ 'version' => $version, diff --git a/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php b/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php index 5b7873b400a7..30e3e11e9f27 100644 --- a/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php +++ b/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php @@ -3,7 +3,6 @@ namespace Illuminate\Http\Resources\Json; use Illuminate\Http\Request; -use Illuminate\Http\Resources\Json\AnonymousResourceCollection; use Illuminate\Support\Collection; class JsonApiResourceCollection extends AnonymousResourceCollection diff --git a/src/Illuminate/Http/Resources/JsonApi/Exceptions/ResourceIdentificationException.php b/src/Illuminate/Http/Resources/JsonApi/Exceptions/ResourceIdentificationException.php index 0a25a5f0eed9..5603dc00522d 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Exceptions/ResourceIdentificationException.php +++ b/src/Illuminate/Http/Resources/JsonApi/Exceptions/ResourceIdentificationException.php @@ -4,7 +4,7 @@ class ResourceIdentificationException extends RuntimeException { - /** + /** * Create a new unable to determine Resource ID exception for the given resource. * * @param mixed $resource diff --git a/src/Illuminate/Http/Resources/JsonApi/Types/Link.php b/src/Illuminate/Http/Resources/JsonApi/Types/Link.php index 276218058bf9..7043f171c970 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Types/Link.php +++ b/src/Illuminate/Http/Resources/JsonApi/Types/Link.php @@ -17,7 +17,8 @@ public function __construct( public string $type, public string $href, public array $meta = [] - ) {} + ) { + } /** * @param array $meta From 5d15ab7094b3f7496b31265ca244b09267f633b1 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Tue, 28 Oct 2025 19:43:18 +0800 Subject: [PATCH 04/74] wip Signed-off-by: Mior Muhammad Zaki --- .../Json/AnonymousJsonApiResource.php | 32 ------------------- .../Http/Resources/Json/JsonApiResource.php | 13 ++++++++ .../Json/JsonApiResourceCollection.php | 2 +- .../Json/JsonApiResourceResponse.php | 17 ++++++++++ 4 files changed, 31 insertions(+), 33 deletions(-) delete mode 100644 src/Illuminate/Http/Resources/Json/AnonymousJsonApiResource.php create mode 100644 src/Illuminate/Http/Resources/Json/JsonApiResourceResponse.php diff --git a/src/Illuminate/Http/Resources/Json/AnonymousJsonApiResource.php b/src/Illuminate/Http/Resources/Json/AnonymousJsonApiResource.php deleted file mode 100644 index 3ad1979d2a3d..000000000000 --- a/src/Illuminate/Http/Resources/Json/AnonymousJsonApiResource.php +++ /dev/null @@ -1,32 +0,0 @@ -version ?? static::$jsonApiVersion; - } - - /** - * Set the JSON:API version for the request. - * - * @param string $version - * @return $this - */ - public function withVersion(string $version) - { - $this->version = $version; - - return $this; - } -} diff --git a/src/Illuminate/Http/Resources/Json/JsonApiResource.php b/src/Illuminate/Http/Resources/Json/JsonApiResource.php index 1eabff4ea7b3..640115bfbe1e 100644 --- a/src/Illuminate/Http/Resources/Json/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonApiResource.php @@ -104,6 +104,19 @@ public function toArray(Request $request) ]; } + + /** + * Create an HTTP response that represents the object. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + #[\Override] + public function toResponse($request) + { + return (new JsonApiResourceResponse($this))->toResponse($request); + } + /** * Customize the outgoing response for the resource. */ diff --git a/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php b/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php index 5b7873b400a7..67af539d8e21 100644 --- a/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php +++ b/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php @@ -50,7 +50,7 @@ public function with($request) ->uniqueStrict(fn (JsonApiResource $resource): array => $resource->uniqueKey($request)) ->values() ->all()) ? ['included' => $included] : [], - ...($implementation = $this->collects::toServerImplementation($request)) // @TODO + ...($implementation = JsonApiResource::$jsonApiInformation) // @TODO ? ['jsonapi' => $implementation] : [], ]; } diff --git a/src/Illuminate/Http/Resources/Json/JsonApiResourceResponse.php b/src/Illuminate/Http/Resources/Json/JsonApiResourceResponse.php new file mode 100644 index 000000000000..21a269b70ef0 --- /dev/null +++ b/src/Illuminate/Http/Resources/Json/JsonApiResourceResponse.php @@ -0,0 +1,17 @@ + Date: Tue, 28 Oct 2025 11:43:51 +0000 Subject: [PATCH 05/74] Apply fixes from StyleCI --- src/Illuminate/Http/Resources/Json/JsonApiResource.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/Json/JsonApiResource.php b/src/Illuminate/Http/Resources/Json/JsonApiResource.php index dac95934b77e..e829ecda1385 100644 --- a/src/Illuminate/Http/Resources/Json/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonApiResource.php @@ -104,7 +104,6 @@ public function toArray(Request $request) ]; } - /** * Create an HTTP response that represents the object. * From c703cf8b2aea8db2957ab431ee0c5b33a12cf179 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Tue, 28 Oct 2025 20:07:55 +0800 Subject: [PATCH 06/74] wip Signed-off-by: Mior Muhammad Zaki --- .../Json/Concerns/InteractsWithMetaInformations.php | 2 +- src/Illuminate/Http/Resources/Json/JsonApiResource.php | 9 ++++----- src/Illuminate/Http/Resources/Json/ResourceResponse.php | 6 +++++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Http/Resources/Json/Concerns/InteractsWithMetaInformations.php b/src/Illuminate/Http/Resources/Json/Concerns/InteractsWithMetaInformations.php index e1e81ef74e4a..cc2b6a9b370b 100644 --- a/src/Illuminate/Http/Resources/Json/Concerns/InteractsWithMetaInformations.php +++ b/src/Illuminate/Http/Resources/Json/Concerns/InteractsWithMetaInformations.php @@ -1,6 +1,6 @@ $profile, 'meta' => $meta, ]); - - return $this; } /** diff --git a/src/Illuminate/Http/Resources/Json/ResourceResponse.php b/src/Illuminate/Http/Resources/Json/ResourceResponse.php index 1c9d7e60f2c3..86907a49c80f 100644 --- a/src/Illuminate/Http/Resources/Json/ResourceResponse.php +++ b/src/Illuminate/Http/Resources/Json/ResourceResponse.php @@ -37,7 +37,11 @@ public function toResponse($request) $this->wrap( $this->resource->resolve($request), $this->resource->with($request), - $this->resource->additional + [ + ...$this->resource->additional, + ...($implementation = JsonApiResource::$jsonApiInformation) + ? ['jsonapi' => $implementation] : [], + ] ), $this->calculateStatus(), [], From b219e6d66bda9d39857ae35fa3d32a0f6d0c0f1c Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 29 Oct 2025 09:45:25 +0800 Subject: [PATCH 07/74] wip Signed-off-by: Mior Muhammad Zaki --- .../InteractsWithMetaInformations.php | 19 ----- .../ResolvesJsonApiSpecifications.php | 70 +++++++++++++++++++ .../Http/Resources/Json/JsonApiResource.php | 16 ++--- 3 files changed, 74 insertions(+), 31 deletions(-) delete mode 100644 src/Illuminate/Http/Resources/Json/Concerns/InteractsWithMetaInformations.php create mode 100644 src/Illuminate/Http/Resources/Json/Concerns/ResolvesJsonApiSpecifications.php diff --git a/src/Illuminate/Http/Resources/Json/Concerns/InteractsWithMetaInformations.php b/src/Illuminate/Http/Resources/Json/Concerns/InteractsWithMetaInformations.php deleted file mode 100644 index cc2b6a9b370b..000000000000 --- a/src/Illuminate/Http/Resources/Json/Concerns/InteractsWithMetaInformations.php +++ /dev/null @@ -1,19 +0,0 @@ - - */ - public function resolveMetaInformations(Request $request): array - { - return array_merge($this->meta($request), $this->meta); - } -} diff --git a/src/Illuminate/Http/Resources/Json/Concerns/ResolvesJsonApiSpecifications.php b/src/Illuminate/Http/Resources/Json/Concerns/ResolvesJsonApiSpecifications.php new file mode 100644 index 000000000000..68bbdedea92f --- /dev/null +++ b/src/Illuminate/Http/Resources/Json/Concerns/ResolvesJsonApiSpecifications.php @@ -0,0 +1,70 @@ +fields($request)); + } + + /** + * Resolves `id` for the resource. + * + * @param \Illuminate\Http\Request $request + * @return string|int + * + * @throws \RuntimeException + */ + protected function resolveResourceIdentifier(Request $request): string|int + { + if ($this->resource instanceof Model) { + return $this->resource->getKey(); + } + + throw new RuntimeException('Unable to determine "type"'); + } + + /** + * Resolves `type` for the resource. + * + * @param \Illuminate\Http\Request $request + * @return string + * + * @throws \RuntimeException + */ + protected function resolveResourceType(Request $request): string + { + if ($this->resource instanceof Model) { + return Str::of(class_basename($this->resource))->snake()->pluralStudly(); + } + + throw new RuntimeException('Unable to determine "type"'); + } + + /** + * Resolves `meta` object for the resource. + * + * @param \Illuminate\Http\Request $request + * @return array + */ + protected function resolveMetaInformations(Request $request): array + { + return array_merge($this->meta($request), $this->meta); + } +} diff --git a/src/Illuminate/Http/Resources/Json/JsonApiResource.php b/src/Illuminate/Http/Resources/Json/JsonApiResource.php index d883328b61d6..6d7e1678ac2d 100644 --- a/src/Illuminate/Http/Resources/Json/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonApiResource.php @@ -11,7 +11,7 @@ abstract class JsonApiResource extends JsonResource { - use Concerns\InteractsWithMetaInformations; + use Concerns\ResolvesJsonApiSpecifications; /** * The resource's "version" for JSON:API. @@ -43,20 +43,12 @@ public function meta(Request $request) public function id(Request $request) { - if ($this->resource instanceof Model) { - return $this->resource->getKey(); - } - - throw new RuntimeException('Unable to determine "id"'); + return $this->resolveResourceIdentifier($request); } public function type(Request $request) { - if ($this->resource instanceof Model) { - return Str::snake(class_basename($this->resource)); - } - - throw new RuntimeException('Unable to determine "type"'); + return $this->resolveResourceType($request); } public function toLinks(Request $request) @@ -95,7 +87,7 @@ public function toArray(Request $request) 'id' => $this->id($request), 'type' => $this->type($request), ...(new Collection([ - // 'attributes' => $this->resolveAttributes($request)->all(), + 'attributes' => $this->resolveResourceAttributes($request), // 'relationships' => $this->resolveRelationshipsAsIdentifiers($request)->all(), // 'links' => self::parseLinks(array_merge($this->toLinks($request), $this->links)), // 'meta' => $this->resolveMetaInformations($request), From 8d2da32967f3e0d823664d2b4215606cbe483d0a Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 29 Oct 2025 01:50:28 +0000 Subject: [PATCH 08/74] Apply fixes from StyleCI --- src/Illuminate/Http/Resources/Json/JsonApiResource.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Illuminate/Http/Resources/Json/JsonApiResource.php b/src/Illuminate/Http/Resources/Json/JsonApiResource.php index 6d7e1678ac2d..427cdb7ddcc5 100644 --- a/src/Illuminate/Http/Resources/Json/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonApiResource.php @@ -2,12 +2,9 @@ namespace Illuminate\Http\Resources\Json; -use Illuminate\Database\Eloquent\Model; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Collection; -use Illuminate\Support\Str; -use RuntimeException; abstract class JsonApiResource extends JsonResource { From e7de4230a3c1b6a069c650c883dd1b704c8dd2f4 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 29 Oct 2025 13:22:34 +0800 Subject: [PATCH 09/74] wip Signed-off-by: Mior Muhammad Zaki --- .../ResolvesJsonApiSpecifications.php | 24 +++++++++++-- .../Http/Resources/Json/JsonApiResource.php | 34 +++++++++++++++---- .../Json/JsonApiResourceCollection.php | 8 +++-- .../Http/Resources/Json/JsonResource.php | 5 +++ .../Http/Resources/Json/ResourceResponse.php | 6 +--- 5 files changed, 59 insertions(+), 18 deletions(-) diff --git a/src/Illuminate/Http/Resources/Json/Concerns/ResolvesJsonApiSpecifications.php b/src/Illuminate/Http/Resources/Json/Concerns/ResolvesJsonApiSpecifications.php index 68bbdedea92f..495551856289 100644 --- a/src/Illuminate/Http/Resources/Json/Concerns/ResolvesJsonApiSpecifications.php +++ b/src/Illuminate/Http/Resources/Json/Concerns/ResolvesJsonApiSpecifications.php @@ -5,6 +5,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Request; use Illuminate\Support\Arr; +use Illuminate\Support\Collection; use Illuminate\Support\Str; use RuntimeException; @@ -20,7 +21,13 @@ trait ResolvesJsonApiSpecifications */ protected function resolveResourceAttributes(Request $request): array { - return Arr::only(parent::toArray($request), $this->fields($request)); + $data = (new Collection($this->toArray($request))) + ->mapWithKeys( + fn ($value, $key) => is_int($key) ? [$value => $this->resource[$value]] : [$key => $value] + )->transform(fn ($value) => value($value, $request)) + ->all(); + + return $this->filter($data); } /** @@ -31,7 +38,7 @@ protected function resolveResourceAttributes(Request $request): array * * @throws \RuntimeException */ - protected function resolveResourceIdentifier(Request $request): string|int + protected function resolveResourceIdentifier(Request $request): string { if ($this->resource instanceof Model) { return $this->resource->getKey(); @@ -57,6 +64,17 @@ protected function resolveResourceType(Request $request): string throw new RuntimeException('Unable to determine "type"'); } + /** + * Get unique key for the resource. + * + * @param \Illuminate\Http\Request $request + * @return array{0: string, 1: string} + */ + public function uniqueResourceKey(Request $request): array + { + return [$this->resolveResourceType($request), $this->resolveResourceIdentifier($request)]; + } + /** * Resolves `meta` object for the resource. * @@ -65,6 +83,6 @@ protected function resolveResourceType(Request $request): string */ protected function resolveMetaInformations(Request $request): array { - return array_merge($this->meta($request), $this->meta); + return array_merge($this->meta($request), $this->with); } } diff --git a/src/Illuminate/Http/Resources/Json/JsonApiResource.php b/src/Illuminate/Http/Resources/Json/JsonApiResource.php index 427cdb7ddcc5..44f2960e8090 100644 --- a/src/Illuminate/Http/Resources/Json/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonApiResource.php @@ -48,13 +48,27 @@ public function type(Request $request) return $this->resolveResourceType($request); } - public function toLinks(Request $request) + public function links(Request $request) { return [ // ]; } + /** + * @param Request $request + * @return array{included?: array, jsonapi: ServerImplementation} + */ + public function with($request) + { + return [ + // ...($included = $this->included($request) + // ->uniqueStrict(fn (JsonApiResource $resource): array => $resource->uniqueResourceKey($request)) + // ->values() + // ->all()) ? ['included' => $included] : [], + ]; + } + /** * Set the JSON:API version for the request. * @@ -72,22 +86,28 @@ public static function configure(?string $version = null, array $ext = [], array } /** - * Transform the resource into an array. + * Resolve the resource to an array. * - * @param \Illuminate\Http\Request $request - * @return array{id: string, type: string, attributes?: stdClass, relationships?: stdClass, meta?: stdClass, links?: stdClass} + * @param \Illuminate\Http\Request|null $request + * @return array */ #[\Override] - public function toArray(Request $request) + public function resolve($request = null) { + $this->additional( + ($implementation = JsonApiResource::$jsonApiInformation) + ? ['jsonapi' => $implementation] + : [] + ); + return [ 'id' => $this->id($request), 'type' => $this->type($request), ...(new Collection([ 'attributes' => $this->resolveResourceAttributes($request), // 'relationships' => $this->resolveRelationshipsAsIdentifiers($request)->all(), - // 'links' => self::parseLinks(array_merge($this->toLinks($request), $this->links)), - // 'meta' => $this->resolveMetaInformations($request), + // 'links' => $this->resolveResourceLinks($request), + 'meta' => $this->resolveMetaInformations($request), ]))->filter()->map(fn ($value) => (object) $value), ]; } diff --git a/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php b/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php index 68db98a8b93d..547780bcc3c7 100644 --- a/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php +++ b/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php @@ -44,9 +44,9 @@ public function with($request) { return [ ...($included = $this->collection - ->map(fn (JsonApiResource $resource): Collection => $resource->included($request)) + //->map(fn (JsonApiResource $resource): Collection => $resource->included($request)) ->flatten() - ->uniqueStrict(fn (JsonApiResource $resource): array => $resource->uniqueKey($request)) + ->uniqueStrict(fn (JsonApiResource $resource): array => $resource->uniqueResourceKey($request)) ->values() ->all()) ? ['included' => $included] : [], ...($implementation = JsonApiResource::$jsonApiInformation) // @TODO @@ -63,7 +63,9 @@ public function with($request) #[\Override] public function toResponse($request) { - return tap(parent::toResponse($request)->header('Content-type', 'application/vnd.api+json'), $this->flush(...)); + return parent::toResponse($request)->header('Content-type', 'application/vnd.api+json'); + + // return tap(parent::toResponse($request)->header('Content-type', 'application/vnd.api+json'), $this->flush(...)); } /** diff --git a/src/Illuminate/Http/Resources/Json/JsonResource.php b/src/Illuminate/Http/Resources/Json/JsonResource.php index f98f2cb53e45..1b98ac066466 100644 --- a/src/Illuminate/Http/Resources/Json/JsonResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonResource.php @@ -274,6 +274,11 @@ public function jsonSerialize(): array return $this->resolve(Container::getInstance()->make('request')); } + public function asJsonApi() + { + return new AnonymousJsonApiResource($this); + } + /** * Flush the resource's global state. * diff --git a/src/Illuminate/Http/Resources/Json/ResourceResponse.php b/src/Illuminate/Http/Resources/Json/ResourceResponse.php index 86907a49c80f..1c9d7e60f2c3 100644 --- a/src/Illuminate/Http/Resources/Json/ResourceResponse.php +++ b/src/Illuminate/Http/Resources/Json/ResourceResponse.php @@ -37,11 +37,7 @@ public function toResponse($request) $this->wrap( $this->resource->resolve($request), $this->resource->with($request), - [ - ...$this->resource->additional, - ...($implementation = JsonApiResource::$jsonApiInformation) - ? ['jsonapi' => $implementation] : [], - ] + $this->resource->additional ), $this->calculateStatus(), [], From 882ed31d3a32e788f3077f8694f5d7f199a762d6 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 29 Oct 2025 05:23:20 +0000 Subject: [PATCH 10/74] Apply fixes from StyleCI --- .../Resources/Json/Concerns/ResolvesJsonApiSpecifications.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/Json/Concerns/ResolvesJsonApiSpecifications.php b/src/Illuminate/Http/Resources/Json/Concerns/ResolvesJsonApiSpecifications.php index 495551856289..a9e81e1c61dc 100644 --- a/src/Illuminate/Http/Resources/Json/Concerns/ResolvesJsonApiSpecifications.php +++ b/src/Illuminate/Http/Resources/Json/Concerns/ResolvesJsonApiSpecifications.php @@ -4,7 +4,6 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Request; -use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Str; use RuntimeException; From 3d75b4c402f7905cc2f7bfff0df29d85a23efb49 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 29 Oct 2025 13:24:55 +0800 Subject: [PATCH 11/74] wip Signed-off-by: Mior Muhammad Zaki --- .../Http/Resources/Json/JsonApiResource.php | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/Illuminate/Http/Resources/Json/JsonApiResource.php b/src/Illuminate/Http/Resources/Json/JsonApiResource.php index 44f2960e8090..5eff0bda7556 100644 --- a/src/Illuminate/Http/Resources/Json/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonApiResource.php @@ -17,14 +17,7 @@ abstract class JsonApiResource extends JsonResource */ public static $jsonApiInformation = []; - public function fields(Request $request) - { - return [ - // - ]; - } - - public function relationships(Request $request) + public function links(Request $request) { return [ // @@ -48,13 +41,6 @@ public function type(Request $request) return $this->resolveResourceType($request); } - public function links(Request $request) - { - return [ - // - ]; - } - /** * @param Request $request * @return array{included?: array, jsonapi: ServerImplementation} From 4aa295d3fa8fd41ccaf7773bc1cd2143a633651b Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 29 Oct 2025 20:53:54 +0800 Subject: [PATCH 12/74] wip Signed-off-by: Mior Muhammad Zaki --- .../ResolvesJsonApiSpecifications.php | 119 ++++++++++++++++-- .../Http/Resources/Json/JsonApiResource.php | 37 +++--- 2 files changed, 129 insertions(+), 27 deletions(-) diff --git a/src/Illuminate/Http/Resources/Json/Concerns/ResolvesJsonApiSpecifications.php b/src/Illuminate/Http/Resources/Json/Concerns/ResolvesJsonApiSpecifications.php index a9e81e1c61dc..8e78c9d64f84 100644 --- a/src/Illuminate/Http/Resources/Json/Concerns/ResolvesJsonApiSpecifications.php +++ b/src/Illuminate/Http/Resources/Json/Concerns/ResolvesJsonApiSpecifications.php @@ -2,14 +2,23 @@ namespace Illuminate\Http\Resources\Json\Concerns; +use Illuminate\Contracts\Support\Arrayable; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Http\Request; use Illuminate\Support\Collection; use Illuminate\Support\Str; +use JsonSerializable; use RuntimeException; +use WeakMap; trait ResolvesJsonApiSpecifications { + /** + * @var \WeakMap|null + */ + protected $cachedLoadedRelationships; + /** * Resolves `attributes` for the resource. * @@ -20,15 +29,73 @@ trait ResolvesJsonApiSpecifications */ protected function resolveResourceAttributes(Request $request): array { - $data = (new Collection($this->toArray($request))) - ->mapWithKeys( - fn ($value, $key) => is_int($key) ? [$value => $this->resource[$value]] : [$key => $value] - )->transform(fn ($value) => value($value, $request)) + $data = $this->toArray($request); + + if ($data instanceof Arrayable) { + $data = $data->toArray(); + } elseif ($data instanceof JsonSerializable) { + $data = $data->jsonSerialize(); + } + + $data = (new Collection($data)) + ->transform(fn ($value) => value($value, $request)) ->all(); return $this->filter($data); } + protected function resolveResourceRelationships(Request $request): array + { + if (! $this->resource instanceof Model) { + return []; + } + + if (is_null($this->cachedLoadedRelationships)) { + $this->cachedLoadedRelationships = new WeakMap; + } + + return [ + 'data' => (new Collection($this->resource->getRelations())) + ->mapWithKeys(function ($relations, $key) { + if ($relations instanceof Collection) { + $key = static::getResourceTypeFromEloquent($relations->first()); + + $relations->each(function ($relation) use ($key) { + $this->cachedLoadedRelationships[$relation] = [$key, $relation->getKey()]; + }); + + return [$key => $relations->map(function ($relation) use ($key) { + return tap([$key, static::getResourceIdFromEloquent($relation)], function ($uniqueKey) use ($relation) { + $this->cachedLoadedRelationships[$relation] = $uniqueKey; + }); + })]; + } + + return tap( + [static::getResourceTypeFromEloquent($relation), static::getResourceIdFromEloquent($relation)], + function ($uniqueKey) use ($relations) { + $this->cachedLoadedRelationships[$relations] = $uniqueKey; + } + ); + }), + ]; + } + + protected function resolveResourceIncluded(Request $request): array + { + $relations = []; + + foreach ($this->cachedLoadedRelationships as $relation => $uniqueKey) { + $relations[] = [ + 'id' => $uniqueKey[1], + 'type' => $uniqueKey[0], + 'attributes' => rescue(fn () => $relation->toResource()->toArray($request), $relation->toArray(), false), + ]; + } + + return $relations; + } + /** * Resolves `id` for the resource. * @@ -40,7 +107,7 @@ protected function resolveResourceAttributes(Request $request): array protected function resolveResourceIdentifier(Request $request): string { if ($this->resource instanceof Model) { - return $this->resource->getKey(); + return static::getResourceIdFromEloquent($this->resource); } throw new RuntimeException('Unable to determine "type"'); @@ -57,7 +124,7 @@ protected function resolveResourceIdentifier(Request $request): string protected function resolveResourceType(Request $request): string { if ($this->resource instanceof Model) { - return Str::of(class_basename($this->resource))->snake()->pluralStudly(); + return static::getResourceTypeFromEloquent($this->resource); } throw new RuntimeException('Unable to determine "type"'); @@ -74,6 +141,17 @@ public function uniqueResourceKey(Request $request): array return [$this->resolveResourceType($request), $this->resolveResourceIdentifier($request)]; } + /** + * Resolves `links` object for the resource. + * + * @param \Illuminate\Http\Request $request + * @return array + */ + protected function resolveResourceLinks(Request $request): array + { + return []; + } + /** * Resolves `meta` object for the resource. * @@ -82,6 +160,33 @@ public function uniqueResourceKey(Request $request): array */ protected function resolveMetaInformations(Request $request): array { - return array_merge($this->meta($request), $this->with); + return $this->meta($request); + } + + /** + * Get expected resource ID from eloquent model. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return string + */ + protected static function getResourceIdFromEloquent(Model $model): string + { + return $model->getKey(); + } + + /** + * Get expected resource type from eloquent model. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return string + */ + protected static function getResourceTypeFromEloquent(Model $model): string + { + $modelClassName = $model::class; + $morphMap = Relation::getMorphAlias($modelClassName); + + $modelBaseName = $morphMap !== $modelClassName ? $morphMap : class_basename($modelClassName); + + return Str::of($modelBaseName)->snake()->pluralStudly(); } } diff --git a/src/Illuminate/Http/Resources/Json/JsonApiResource.php b/src/Illuminate/Http/Resources/Json/JsonApiResource.php index 5eff0bda7556..41d1d4cbb25f 100644 --- a/src/Illuminate/Http/Resources/Json/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonApiResource.php @@ -4,6 +4,7 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Http\Resources\MissingValue; use Illuminate\Support\Collection; abstract class JsonApiResource extends JsonResource @@ -47,12 +48,12 @@ public function type(Request $request) */ public function with($request) { - return [ - // ...($included = $this->included($request) - // ->uniqueStrict(fn (JsonApiResource $resource): array => $resource->uniqueResourceKey($request)) - // ->values() - // ->all()) ? ['included' => $included] : [], - ]; + return array_filter([ + 'included' => $this->resolveResourceIncluded($request), + ...($implementation = static::$jsonApiInformation) + ? ['jsonapi' => $implementation] + : [], + ]); } /** @@ -80,21 +81,17 @@ public static function configure(?string $version = null, array $ext = [], array #[\Override] public function resolve($request = null) { - $this->additional( - ($implementation = JsonApiResource::$jsonApiInformation) - ? ['jsonapi' => $implementation] - : [] - ); - return [ - 'id' => $this->id($request), - 'type' => $this->type($request), - ...(new Collection([ - 'attributes' => $this->resolveResourceAttributes($request), - // 'relationships' => $this->resolveRelationshipsAsIdentifiers($request)->all(), - // 'links' => $this->resolveResourceLinks($request), - 'meta' => $this->resolveMetaInformations($request), - ]))->filter()->map(fn ($value) => (object) $value), + 'data' => [ + 'id' => $this->resolveResourceIdentifier($request), + 'type' => $this->resolveResourceType($request), + ...(new Collection([ + 'attributes' => $this->resolveResourceAttributes($request), + 'relationships' => $this->resolveResourceRelationships($request), + 'links' => $this->resolveResourceLinks($request), + 'meta' => $this->resolveMetaInformations($request), + ]))->filter()->map(fn ($value) => (object) $value), + ], ]; } From d945d9faa39120d4a93d6dddc28ded184081f49f Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 29 Oct 2025 12:54:09 +0000 Subject: [PATCH 13/74] Apply fixes from StyleCI --- src/Illuminate/Http/Resources/Json/JsonApiResource.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/Json/JsonApiResource.php b/src/Illuminate/Http/Resources/Json/JsonApiResource.php index 41d1d4cbb25f..ff1176c7db37 100644 --- a/src/Illuminate/Http/Resources/Json/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonApiResource.php @@ -4,7 +4,6 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Http\Resources\MissingValue; use Illuminate\Support\Collection; abstract class JsonApiResource extends JsonResource From c2d7dfc9ce81e4c236ddd417b1d98672e7947c43 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 08:48:23 +0800 Subject: [PATCH 14/74] wip Signed-off-by: Mior Muhammad Zaki --- .../ResolvesJsonApiSpecifications.php | 18 ++ .../Http/Resources/Json/JsonApiResource.php | 13 +- .../Json/JsonApiResourceCollection.php | 275 +++++++++--------- 3 files changed, 164 insertions(+), 142 deletions(-) diff --git a/src/Illuminate/Http/Resources/Json/Concerns/ResolvesJsonApiSpecifications.php b/src/Illuminate/Http/Resources/Json/Concerns/ResolvesJsonApiSpecifications.php index 8e78c9d64f84..6fe07d41e5bc 100644 --- a/src/Illuminate/Http/Resources/Json/Concerns/ResolvesJsonApiSpecifications.php +++ b/src/Illuminate/Http/Resources/Json/Concerns/ResolvesJsonApiSpecifications.php @@ -44,6 +44,20 @@ protected function resolveResourceAttributes(Request $request): array return $this->filter($data); } + public function resolveResourceData(Request $request): array + { + return [ + 'id' => $this->resolveResourceIdentifier($request), + 'type' => $this->resolveResourceType($request), + ...(new Collection([ + 'attributes' => $this->resolveResourceAttributes($request), + 'relationships' => $this->resolveResourceRelationships($request), + 'links' => $this->resolveResourceLinks($request), + 'meta' => $this->resolveMetaInformations($request), + ]))->filter()->map(fn ($value) => (object) $value), + ]; + } + protected function resolveResourceRelationships(Request $request): array { if (! $this->resource instanceof Model) { @@ -58,6 +72,10 @@ protected function resolveResourceRelationships(Request $request): array 'data' => (new Collection($this->resource->getRelations())) ->mapWithKeys(function ($relations, $key) { if ($relations instanceof Collection) { + if ($relations->isEmpty()) { + return [$key => $relations]; + } + $key = static::getResourceTypeFromEloquent($relations->first()); $relations->each(function ($relation) use ($key) { diff --git a/src/Illuminate/Http/Resources/Json/JsonApiResource.php b/src/Illuminate/Http/Resources/Json/JsonApiResource.php index 41d1d4cbb25f..fb8cf4f13430 100644 --- a/src/Illuminate/Http/Resources/Json/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonApiResource.php @@ -7,7 +7,7 @@ use Illuminate\Http\Resources\MissingValue; use Illuminate\Support\Collection; -abstract class JsonApiResource extends JsonResource +class JsonApiResource extends JsonResource { use Concerns\ResolvesJsonApiSpecifications; @@ -82,16 +82,7 @@ public static function configure(?string $version = null, array $ext = [], array public function resolve($request = null) { return [ - 'data' => [ - 'id' => $this->resolveResourceIdentifier($request), - 'type' => $this->resolveResourceType($request), - ...(new Collection([ - 'attributes' => $this->resolveResourceAttributes($request), - 'relationships' => $this->resolveResourceRelationships($request), - 'links' => $this->resolveResourceLinks($request), - 'meta' => $this->resolveMetaInformations($request), - ]))->filter()->map(fn ($value) => (object) $value), - ], + 'data' => $this->resolveResourceToJsonApiSchema($request), ]; } diff --git a/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php b/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php index 547780bcc3c7..cc4265300fe4 100644 --- a/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php +++ b/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php @@ -8,141 +8,154 @@ class JsonApiResourceCollection extends AnonymousResourceCollection { /** - * @param (callable(\Illuminate\Http\Resources\JsonApi\JsonApiResource): \Illuminate\Http\Resources\JsonApi\JsonApiResource) $callback - * @return $this - */ - public function map(callable $callback) - { - $this->collection = $this->collection->map($callback); - - return $this; - } - - /** - * @return RelationshipObject - */ - public function toResourceLink(Request $request) - { - return RelationshipObject::toMany($this->resolveResourceIdentifiers($request)->all()); - } - - /** - * @return Collection - */ - private function resolveResourceIdentifiers(Request $request) - { - return $this->collection - ->uniqueStrict(fn (JsonApiResource $resource): array => $resource->uniqueKey($request)) - ->map(fn (JsonApiResource $resource): ResourceIdentifier => $resource->resolveResourceIdentifier($request)); - } - - /** - * @param \Illuminate\Http\Request $request - * @return array{included?: array, jsonapi?: ServerImplementation} - */ - public function with($request) - { - return [ - ...($included = $this->collection - //->map(fn (JsonApiResource $resource): Collection => $resource->included($request)) - ->flatten() - ->uniqueStrict(fn (JsonApiResource $resource): array => $resource->uniqueResourceKey($request)) - ->values() - ->all()) ? ['included' => $included] : [], - ...($implementation = JsonApiResource::$jsonApiInformation) // @TODO - ? ['jsonapi' => $implementation] : [], - ]; - } - - /** - * 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($request)->header('Content-type', 'application/vnd.api+json'); - - // return tap(parent::toResponse($request)->header('Content-type', 'application/vnd.api+json'), $this->flush(...)); - } - - /** - * @param \Illuminate\Http\Request $request - * @param array $paginated - * @param array{links: array} $default - * @return array{links: array} - */ - public function paginationInformation(Request $request, array $paginated, array $default) - { - if (isset($default['links'])) { - $default['links'] = array_filter($default['links'], fn (?string $link): bool => $link !== null); - } - - if (isset($default['meta']['links'])) { - $default['meta']['links'] = array_map( - function (array $link): array { - $link['label'] = (string) $link['label']; - - return $link; - }, - $default['meta']['links'] - ); - } - - return $default; - } - - /** - * Set include prefix to resources. - * - * @internal - * - * @param string $prefix - * @return $this - */ - public function withIncludePrefix(string $prefix) - { - $this->collection->each(fn (JsonApiResource $resource): JsonApiResource => $resource->withIncludePrefix($prefix)); - - return $this; - } - - /** - * Get included resources. - * - * @internal + * Transform the resource into a JSON array. * * @param \Illuminate\Http\Request $request - * @return \Illuminate\Support\Collection> - */ - public function included(Request $request) - { - return $this->collection->map(fn (JsonApiResource $resource): Collection => $resource->included($request)); - } - - /** - * Get the includable collection. - * - * @internal - * - * @return \Illuminate\Support\Collection + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable */ - public function includable() + public function toArray(Request $request) { - return $this->collection; + return $this->collection + ->map(fn ($resource) => $resource->resolveResourceData($request)) + ->all(); } - /** - * Flush resource collection states. - * - * @internal - * - * @return void - */ - public function flush(): void - { - $this->collection->each(fn (JsonApiResource $resource) => $resource->flush()); - } + // /** + // * @param (callable(\Illuminate\Http\Resources\JsonApi\JsonApiResource): \Illuminate\Http\Resources\JsonApi\JsonApiResource) $callback + // * @return $this + // */ + // public function map(callable $callback) + // { + // $this->collection = $this->collection->map($callback); + + // return $this; + // } + + // /** + // * @return RelationshipObject + // */ + // public function toResourceLink(Request $request) + // { + // return RelationshipObject::toMany($this->resolveResourceIdentifiers($request)->all()); + // } + + // /** + // * @return Collection + // */ + // private function resolveResourceIdentifiers(Request $request) + // { + // return $this->collection + // ->uniqueStrict(fn (JsonApiResource $resource): array => $resource->uniqueKey($request)) + // ->map(fn (JsonApiResource $resource): ResourceIdentifier => $resource->resolveResourceIdentifier($request)); + // } + + // /** + // * @param \Illuminate\Http\Request $request + // * @return array{included?: array, jsonapi?: ServerImplementation} + // */ + // public function with($request) + // { + // return [ + // ...($included = $this->collection + // //->map(fn (JsonApiResource $resource): Collection => $resource->included($request)) + // ->flatten() + // ->uniqueStrict(fn (JsonApiResource $resource): array => $resource->uniqueResourceKey($request)) + // ->values() + // ->all()) ? ['included' => $included] : [], + // ...($implementation = JsonApiResource::$jsonApiInformation) // @TODO + // ? ['jsonapi' => $implementation] : [], + // ]; + // } + + // /** + // * 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($request)->header('Content-type', 'application/vnd.api+json'); + + // // return tap(parent::toResponse($request)->header('Content-type', 'application/vnd.api+json'), $this->flush(...)); + // } + + // /** + // * @param \Illuminate\Http\Request $request + // * @param array $paginated + // * @param array{links: array} $default + // * @return array{links: array} + // */ + // public function paginationInformation(Request $request, array $paginated, array $default) + // { + // if (isset($default['links'])) { + // $default['links'] = array_filter($default['links'], fn (?string $link): bool => $link !== null); + // } + + // if (isset($default['meta']['links'])) { + // $default['meta']['links'] = array_map( + // function (array $link): array { + // $link['label'] = (string) $link['label']; + + // return $link; + // }, + // $default['meta']['links'] + // ); + // } + + // return $default; + // } + + // /** + // * Set include prefix to resources. + // * + // * @internal + // * + // * @param string $prefix + // * @return $this + // */ + // public function withIncludePrefix(string $prefix) + // { + // $this->collection->each(fn (JsonApiResource $resource): JsonApiResource => $resource->withIncludePrefix($prefix)); + + // return $this; + // } + + // /** + // * Get included resources. + // * + // * @internal + // * + // * @param \Illuminate\Http\Request $request + // * @return \Illuminate\Support\Collection> + // */ + // public function included(Request $request) + // { + // return $this->collection->map(fn (JsonApiResource $resource): Collection => $resource->included($request)); + // } + + // /** + // * Get the includable collection. + // * + // * @internal + // * + // * @return \Illuminate\Support\Collection + // */ + // public function includable() + // { + // return $this->collection; + // } + + // /** + // * Flush resource collection states. + // * + // * @internal + // * + // * @return void + // */ + // public function flush(): void + // { + // $this->collection->each(fn (JsonApiResource $resource) => $resource->flush()); + // } } From 2b6998478ba2d8786450d840e096df09608869a2 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 09:44:59 +0800 Subject: [PATCH 15/74] wip Signed-off-by: Mior Muhammad Zaki --- .../ResolvesJsonApiSpecifications.php | 90 +++++++++++-------- .../Http/Resources/Json/JsonApiResource.php | 2 +- .../Json/JsonApiResourceCollection.php | 19 ++++ 3 files changed, 73 insertions(+), 38 deletions(-) diff --git a/src/Illuminate/Http/Resources/Json/Concerns/ResolvesJsonApiSpecifications.php b/src/Illuminate/Http/Resources/Json/Concerns/ResolvesJsonApiSpecifications.php index 6fe07d41e5bc..75c01d9391ba 100644 --- a/src/Illuminate/Http/Resources/Json/Concerns/ResolvesJsonApiSpecifications.php +++ b/src/Illuminate/Http/Resources/Json/Concerns/ResolvesJsonApiSpecifications.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Http\Request; +use Illuminate\Http\Resources\Json\JsonApiResource; use Illuminate\Support\Collection; use Illuminate\Support\Str; use JsonSerializable; @@ -17,7 +18,12 @@ trait ResolvesJsonApiSpecifications /** * @var \WeakMap|null */ - protected $cachedLoadedRelationships; + protected $cachedLoadedRelationshipsMap; + + /** + * @var array + */ + protected array $cachedLoadedRelationshipsIdentifier = []; /** * Resolves `attributes` for the resource. @@ -51,67 +57,77 @@ public function resolveResourceData(Request $request): array 'type' => $this->resolveResourceType($request), ...(new Collection([ 'attributes' => $this->resolveResourceAttributes($request), - 'relationships' => $this->resolveResourceRelationships($request), + 'relationships' => $this->resolveResourceRelationshipsIdentifiers($request), 'links' => $this->resolveResourceLinks($request), 'meta' => $this->resolveMetaInformations($request), ]))->filter()->map(fn ($value) => (object) $value), ]; } - protected function resolveResourceRelationships(Request $request): array + protected function resolveResourceRelationships(Request $request): void { - if (! $this->resource instanceof Model) { - return []; + if ($this->cachedLoadedRelationships instanceof WeakMap) { + return; } - if (is_null($this->cachedLoadedRelationships)) { - $this->cachedLoadedRelationships = new WeakMap; - } + $this->cachedLoadedRelationshipsMap = new WeakMap; - return [ - 'data' => (new Collection($this->resource->getRelations())) - ->mapWithKeys(function ($relations, $key) { - if ($relations instanceof Collection) { - if ($relations->isEmpty()) { - return [$key => $relations]; - } + $this->cachedLoadedRelationshipsIdentifier = (new Collection($this->resource->getRelations())) + ->mapWithKeys(function ($relations, $key) { + if ($relations instanceof Collection) { + if ($relations->isEmpty()) { + return [$key => $relations]; + } + + $key = static::getResourceTypeFromEloquent($relations->first()); - $key = static::getResourceTypeFromEloquent($relations->first()); + return [$key => $relations->map(function ($relation) use ($key) { + return transform([$key, static::getResourceIdFromEloquent($relation)], function ($uniqueKey) use ($relation) { + $this->cachedLoadedRelationshipsMap[$relation] = $uniqueKey; - $relations->each(function ($relation) use ($key) { - $this->cachedLoadedRelationships[$relation] = [$key, $relation->getKey()]; + return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]]; }); + })]; + } - return [$key => $relations->map(function ($relation) use ($key) { - return tap([$key, static::getResourceIdFromEloquent($relation)], function ($uniqueKey) use ($relation) { - $this->cachedLoadedRelationships[$relation] = $uniqueKey; - }); - })]; + return transform( + [static::getResourceTypeFromEloquent($relation), static::getResourceIdFromEloquent($relation)], + function ($uniqueKey) { + $this->cachedLoadedRelationshipsMap[$relation] = $uniqueKey; + + return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]]; } + ); + })->all(); + } - return tap( - [static::getResourceTypeFromEloquent($relation), static::getResourceIdFromEloquent($relation)], - function ($uniqueKey) use ($relations) { - $this->cachedLoadedRelationships[$relations] = $uniqueKey; - } - ); - }), + protected function resolveResourceRelationshipsIdentifiers(Request $request): array + { + if (! $this->resource instanceof Model) { + return []; + } + + $this->resolveResourceRelationships($request); + + return [ + 'data' => $this->cachedLoadedRelationshipsIdentifier, ]; } - protected function resolveResourceIncluded(Request $request): array + public function resolveResourceIncluded(Request $request): array { - $relations = []; + $relations = new Collection(); - foreach ($this->cachedLoadedRelationships as $relation => $uniqueKey) { - $relations[] = [ + foreach ($this->cachedLoadedRelationshipsMap as $relation => $uniqueKey) { + $resource = rescue(fn () => $relation->toResource(), new JsonApiResource($relation), false); + $relations->push([ 'id' => $uniqueKey[1], 'type' => $uniqueKey[0], - 'attributes' => rescue(fn () => $relation->toResource()->toArray($request), $relation->toArray(), false), - ]; + 'attributes' => $resource->toArray($request), + ]); } - return $relations; + return $relations->uniqueStrict(fn ($relation): array => [$relation['id'], $relation['type']])->all(); } /** diff --git a/src/Illuminate/Http/Resources/Json/JsonApiResource.php b/src/Illuminate/Http/Resources/Json/JsonApiResource.php index 29c0e8638236..91464f76e9b2 100644 --- a/src/Illuminate/Http/Resources/Json/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonApiResource.php @@ -81,7 +81,7 @@ public static function configure(?string $version = null, array $ext = [], array public function resolve($request = null) { return [ - 'data' => $this->resolveResourceToJsonApiSchema($request), + 'data' => $this->resolveResourceData($request), ]; } diff --git a/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php b/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php index cc4265300fe4..f9a101075923 100644 --- a/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php +++ b/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php @@ -7,6 +7,25 @@ class JsonApiResourceCollection extends AnonymousResourceCollection { + + /** + * @param Request $request + * @return array{included?: array, jsonapi: ServerImplementation} + */ + public function with($request) + { + return array_filter([ + 'included' => $this->collection + ->map(fn ($resource) => $resource->resolveResourceIncluded($request)) + ->flatten(depth: 1) + ->uniqueStrict(fn ($relation): array => [$relation['id'], $relation['type']]) + ->all(), + ...($implementation = JsonApiResource::$jsonApiInformation) + ? ['jsonapi' => $implementation] + : [], + ]); + } + /** * Transform the resource into a JSON array. * From b00640171b73faf3bbcb41a32071cb114e464894 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 30 Oct 2025 01:45:25 +0000 Subject: [PATCH 16/74] Apply fixes from StyleCI --- src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php b/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php index f9a101075923..b16c374bd97e 100644 --- a/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php +++ b/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php @@ -7,7 +7,6 @@ class JsonApiResourceCollection extends AnonymousResourceCollection { - /** * @param Request $request * @return array{included?: array, jsonapi: ServerImplementation} From 174b4a8d82deadfc3ea86dd61d752d66d2106658 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 09:45:52 +0800 Subject: [PATCH 17/74] wip Signed-off-by: Mior Muhammad Zaki --- .../Json/JsonApiResourceCollection.php | 139 ------------------ 1 file changed, 139 deletions(-) diff --git a/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php b/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php index b16c374bd97e..2abd2f2b8080 100644 --- a/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php +++ b/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php @@ -37,143 +37,4 @@ public function toArray(Request $request) ->map(fn ($resource) => $resource->resolveResourceData($request)) ->all(); } - - // /** - // * @param (callable(\Illuminate\Http\Resources\JsonApi\JsonApiResource): \Illuminate\Http\Resources\JsonApi\JsonApiResource) $callback - // * @return $this - // */ - // public function map(callable $callback) - // { - // $this->collection = $this->collection->map($callback); - - // return $this; - // } - - // /** - // * @return RelationshipObject - // */ - // public function toResourceLink(Request $request) - // { - // return RelationshipObject::toMany($this->resolveResourceIdentifiers($request)->all()); - // } - - // /** - // * @return Collection - // */ - // private function resolveResourceIdentifiers(Request $request) - // { - // return $this->collection - // ->uniqueStrict(fn (JsonApiResource $resource): array => $resource->uniqueKey($request)) - // ->map(fn (JsonApiResource $resource): ResourceIdentifier => $resource->resolveResourceIdentifier($request)); - // } - - // /** - // * @param \Illuminate\Http\Request $request - // * @return array{included?: array, jsonapi?: ServerImplementation} - // */ - // public function with($request) - // { - // return [ - // ...($included = $this->collection - // //->map(fn (JsonApiResource $resource): Collection => $resource->included($request)) - // ->flatten() - // ->uniqueStrict(fn (JsonApiResource $resource): array => $resource->uniqueResourceKey($request)) - // ->values() - // ->all()) ? ['included' => $included] : [], - // ...($implementation = JsonApiResource::$jsonApiInformation) // @TODO - // ? ['jsonapi' => $implementation] : [], - // ]; - // } - - // /** - // * 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($request)->header('Content-type', 'application/vnd.api+json'); - - // // return tap(parent::toResponse($request)->header('Content-type', 'application/vnd.api+json'), $this->flush(...)); - // } - - // /** - // * @param \Illuminate\Http\Request $request - // * @param array $paginated - // * @param array{links: array} $default - // * @return array{links: array} - // */ - // public function paginationInformation(Request $request, array $paginated, array $default) - // { - // if (isset($default['links'])) { - // $default['links'] = array_filter($default['links'], fn (?string $link): bool => $link !== null); - // } - - // if (isset($default['meta']['links'])) { - // $default['meta']['links'] = array_map( - // function (array $link): array { - // $link['label'] = (string) $link['label']; - - // return $link; - // }, - // $default['meta']['links'] - // ); - // } - - // return $default; - // } - - // /** - // * Set include prefix to resources. - // * - // * @internal - // * - // * @param string $prefix - // * @return $this - // */ - // public function withIncludePrefix(string $prefix) - // { - // $this->collection->each(fn (JsonApiResource $resource): JsonApiResource => $resource->withIncludePrefix($prefix)); - - // return $this; - // } - - // /** - // * Get included resources. - // * - // * @internal - // * - // * @param \Illuminate\Http\Request $request - // * @return \Illuminate\Support\Collection> - // */ - // public function included(Request $request) - // { - // return $this->collection->map(fn (JsonApiResource $resource): Collection => $resource->included($request)); - // } - - // /** - // * Get the includable collection. - // * - // * @internal - // * - // * @return \Illuminate\Support\Collection - // */ - // public function includable() - // { - // return $this->collection; - // } - - // /** - // * Flush resource collection states. - // * - // * @internal - // * - // * @return void - // */ - // public function flush(): void - // { - // $this->collection->each(fn (JsonApiResource $resource) => $resource->flush()); - // } } From 13fedebbe5ff3264b3c44d76af477a39dde2adde Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 30 Oct 2025 03:32:34 +0000 Subject: [PATCH 18/74] Apply fixes from StyleCI --- src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php b/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php index 2abd2f2b8080..20ead67e54e8 100644 --- a/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php +++ b/src/Illuminate/Http/Resources/Json/JsonApiResourceCollection.php @@ -3,7 +3,6 @@ namespace Illuminate\Http\Resources\Json; use Illuminate\Http\Request; -use Illuminate\Support\Collection; class JsonApiResourceCollection extends AnonymousResourceCollection { From 5c159204bb496ba6d4bf32b5f564d9642778dd94 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 11:34:14 +0800 Subject: [PATCH 19/74] wip Signed-off-by: Mior Muhammad Zaki --- .../Http/Resources/JsonApi/Types/Link.php | 53 ------------------- 1 file changed, 53 deletions(-) delete mode 100644 src/Illuminate/Http/Resources/JsonApi/Types/Link.php diff --git a/src/Illuminate/Http/Resources/JsonApi/Types/Link.php b/src/Illuminate/Http/Resources/JsonApi/Types/Link.php deleted file mode 100644 index 7043f171c970..000000000000 --- a/src/Illuminate/Http/Resources/JsonApi/Types/Link.php +++ /dev/null @@ -1,53 +0,0 @@ - $meta - * @return static - */ - public static function to(string $href, array $meta = []) - { - return new static('self', $href, $meta); - } - - /** - * @param array $meta - * @return static - */ - public static function related(string $href, array $meta = []) - { - return new static('related', $href, $meta); - } - - /** - * Prepare the link for JSON serialization. - * - * @return array{href: string, meta?: object} - */ - public function jsonSerialize(): array - { - return [ - 'href' => $this->href, - ...$this->meta ? ['meta' => (object) $this->meta] : [], - ]; - } -} From 0319cbea8078dff913d32b21364e91bde2dce06a Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 11:36:56 +0800 Subject: [PATCH 20/74] wip Signed-off-by: Mior Muhammad Zaki --- .../Testing/Concerns/InteractsWithTestCaseLifecycle.php | 4 ++++ .../Http/Resources/{Json => JsonApi}/JsonApiResource.php | 1 + 2 files changed, 5 insertions(+) rename src/Illuminate/Http/Resources/{Json => JsonApi}/JsonApiResource.php (98%) diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php index 0daabf1ce139..98a2fe66b903 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php @@ -23,6 +23,8 @@ use Illuminate\Foundation\Testing\WithoutMiddleware; use Illuminate\Http\Middleware\TrustHosts; use Illuminate\Http\Middleware\TrustProxies; +use Illuminate\Http\Resources\JsonApi\JsonApiResource; +use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Mail\Markdown; use Illuminate\Queue\Console\WorkCommand; use Illuminate\Queue\Queue; @@ -177,6 +179,8 @@ protected function tearDownTheTestEnvironment(): void EncodedHtmlString::flushState(); EncryptCookies::flushState(); HandleExceptions::flushState($this); + JsonApiResource::flushState(); + JsonResource::flushState(); Markdown::flushState(); Migrator::withoutMigrations([]); Once::flush(); diff --git a/src/Illuminate/Http/Resources/Json/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php similarity index 98% rename from src/Illuminate/Http/Resources/Json/JsonApiResource.php rename to src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index 91464f76e9b2..f1dfdc9c1f59 100644 --- a/src/Illuminate/Http/Resources/Json/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -4,6 +4,7 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Http\Resources\Json\JsonResponse; use Illuminate\Support\Collection; class JsonApiResource extends JsonResource From a3ee8f728e9509a9f1d6b538a6334ed6d69f77bf Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 11:39:24 +0800 Subject: [PATCH 21/74] wip Signed-off-by: Mior Muhammad Zaki --- .../Http/Resources/JsonApi/AnonymousJsonApiResource.php | 8 ++++++++ .../Concerns/ResolvesJsonApiSpecifications.php | 2 +- src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php | 2 +- .../{Json => JsonApi}/JsonApiResourceCollection.php | 3 ++- .../{Json => JsonApi}/JsonApiResourceResponse.php | 4 +++- 5 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php rename src/Illuminate/Http/Resources/{Json => JsonApi}/Concerns/ResolvesJsonApiSpecifications.php (99%) rename src/Illuminate/Http/Resources/{Json => JsonApi}/JsonApiResourceCollection.php (91%) rename src/Illuminate/Http/Resources/{Json => JsonApi}/JsonApiResourceResponse.php (71%) diff --git a/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php new file mode 100644 index 000000000000..c3282551ce54 --- /dev/null +++ b/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php @@ -0,0 +1,8 @@ + Date: Thu, 30 Oct 2025 03:40:12 +0000 Subject: [PATCH 22/74] Apply fixes from StyleCI --- .../Testing/Concerns/InteractsWithTestCaseLifecycle.php | 2 +- .../Http/Resources/JsonApi/AnonymousJsonApiResource.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php index 98a2fe66b903..34077fb3224f 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php @@ -23,8 +23,8 @@ use Illuminate\Foundation\Testing\WithoutMiddleware; use Illuminate\Http\Middleware\TrustHosts; use Illuminate\Http\Middleware\TrustProxies; -use Illuminate\Http\Resources\JsonApi\JsonApiResource; use Illuminate\Http\Resources\Json\JsonResource; +use Illuminate\Http\Resources\JsonApi\JsonApiResource; use Illuminate\Mail\Markdown; use Illuminate\Queue\Console\WorkCommand; use Illuminate\Queue\Queue; diff --git a/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php index c3282551ce54..cc533dca93c9 100644 --- a/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php @@ -4,5 +4,4 @@ class AnonymousJsonApiResource extends JsonApiResource { - } From e7a8d6e634b4737c909dd4f5457a253ba98b2549 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 12:02:02 +0800 Subject: [PATCH 23/74] wip Signed-off-by: Mior Muhammad Zaki --- .../Http/Resources/Json/JsonResource.php | 8 +++++- .../JsonApi/AnonymousJsonApiResource.php | 25 +++++++++++++++++++ .../ResolvesJsonApiSpecifications.php | 4 +-- .../Resources/JsonApi/JsonApiResource.php | 16 +++++++++--- ...eCollection.php => ResourceCollection.php} | 2 +- ...ourceResponse.php => ResourceResponse.php} | 4 +-- 6 files changed, 49 insertions(+), 10 deletions(-) rename src/Illuminate/Http/Resources/JsonApi/{JsonApiResourceCollection.php => ResourceCollection.php} (94%) rename src/Illuminate/Http/Resources/JsonApi/{JsonApiResourceResponse.php => ResourceResponse.php} (68%) diff --git a/src/Illuminate/Http/Resources/Json/JsonResource.php b/src/Illuminate/Http/Resources/Json/JsonResource.php index 1b98ac066466..af2d4bb0f5d2 100644 --- a/src/Illuminate/Http/Resources/Json/JsonResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonResource.php @@ -12,6 +12,7 @@ use Illuminate\Http\Request; use Illuminate\Http\Resources\ConditionallyLoadsAttributes; use Illuminate\Http\Resources\DelegatesToResource; +use Illuminate\Http\Resources\JsonApi\AnonymousJsonApiResource; use JsonException; use JsonSerializable; @@ -274,9 +275,14 @@ public function jsonSerialize(): array return $this->resolve(Container::getInstance()->make('request')); } + /** + * Transform JSON resource to JSON:API. + * + * @return \Illuminate\Http\Resources\JsonApi\AnonymousJsonApiResource + */ public function asJsonApi() { - return new AnonymousJsonApiResource($this); + return new AnonymousJsonApiResource($this->resource, $this); } /** diff --git a/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php index cc533dca93c9..13af71fbf033 100644 --- a/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php @@ -2,6 +2,31 @@ namespace Illuminate\Http\Resources\JsonApi; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Http\Request; +use Illuminate\Http\Resources\Json\JsonResource; +use Illuminate\Support\Arr; + class AnonymousJsonApiResource extends JsonApiResource { + /** + * Create a new resource instance. + * + * @param mixed $resource + */ + public function __construct($resource, protected JsonResource $source) + { + parent::__construct($resource); + } + + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray(Request $request) + { + return $this->source->toArray($request); + } } diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php index c5d140a7467b..346ba458bead 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php @@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Http\Request; -use Illuminate\Http\Resources\Json\JsonApiResource; +use Illuminate\Http\Resources\JsonApi\JsonApiResource; use Illuminate\Support\Collection; use Illuminate\Support\Str; use JsonSerializable; @@ -123,7 +123,7 @@ public function resolveResourceIncluded(Request $request): array $relations->push([ 'id' => $uniqueKey[1], 'type' => $uniqueKey[0], - 'attributes' => $resource->toArray($request), + 'attributes' => $resource->asJsonApi()->toArray($request), ]); } diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index e7a58edfa944..5cf981c01b2e 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -4,7 +4,7 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Http\Resources\Json\JsonResponse; +use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Support\Collection; class JsonApiResource extends JsonResource @@ -95,7 +95,7 @@ public function resolve($request = null) #[\Override] public function toResponse($request) { - return (new JsonApiResourceResponse($this))->toResponse($request); + return (new ResourceResponse($this))->toResponse($request); } /** @@ -107,6 +107,16 @@ public function withResponse(Request $request, JsonResponse $response): void $response->header('Content-type', 'application/vnd.api+json'); } + + /** + * {@inheritdoc} + */ + #[\Override] + public function asJsonApi() + { + return $this; + } + /** * Create a new resource collection instance. * @@ -116,7 +126,7 @@ public function withResponse(Request $request, JsonResponse $response): void #[\Override] protected static function newCollection($resource) { - return new JsonApiResourceCollection($resource, static::class); + return new ResourceCollection($resource, static::class); } /** diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResourceCollection.php b/src/Illuminate/Http/Resources/JsonApi/ResourceCollection.php similarity index 94% rename from src/Illuminate/Http/Resources/JsonApi/JsonApiResourceCollection.php rename to src/Illuminate/Http/Resources/JsonApi/ResourceCollection.php index 7911b4888f9e..9dfe6d6067f4 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResourceCollection.php +++ b/src/Illuminate/Http/Resources/JsonApi/ResourceCollection.php @@ -5,7 +5,7 @@ use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\AnonymousResourceCollection; -class JsonApiResourceCollection extends AnonymousResourceCollection +class ResourceCollection extends AnonymousResourceCollection { /** * @param Request $request diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResourceResponse.php b/src/Illuminate/Http/Resources/JsonApi/ResourceResponse.php similarity index 68% rename from src/Illuminate/Http/Resources/JsonApi/JsonApiResourceResponse.php rename to src/Illuminate/Http/Resources/JsonApi/ResourceResponse.php index 7eb9228b832d..3fdbcc935336 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResourceResponse.php +++ b/src/Illuminate/Http/Resources/JsonApi/ResourceResponse.php @@ -2,9 +2,7 @@ namespace Illuminate\Http\Resources\JsonApi; -use Illuminate\Http\Resources\Json\ResourceResponse; - -class JsonApiResourceResponse extends ResourceResponse +class ResourceResponse extends \Illuminate\Http\Resources\Json\ResourceResponse { /** * Get the default data wrapper for the resource. From f153b5eb015adb4de24f1dcd5ec9c4e1eea49913 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 30 Oct 2025 04:02:17 +0000 Subject: [PATCH 24/74] Apply fixes from StyleCI --- .../Http/Resources/JsonApi/AnonymousJsonApiResource.php | 2 -- src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php | 1 - 2 files changed, 3 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php index 13af71fbf033..9beee43dc854 100644 --- a/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php @@ -2,10 +2,8 @@ namespace Illuminate\Http\Resources\JsonApi; -use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; -use Illuminate\Support\Arr; class AnonymousJsonApiResource extends JsonApiResource { diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index 5cf981c01b2e..d0cef7666fa8 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -107,7 +107,6 @@ public function withResponse(Request $request, JsonResponse $response): void $response->header('Content-type', 'application/vnd.api+json'); } - /** * {@inheritdoc} */ From dab4d402623378bd6a257e38ed7e91b52b0c08c1 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 12:13:26 +0800 Subject: [PATCH 25/74] wip Signed-off-by: Mior Muhammad Zaki --- .../Http/Resources/JsonApi/ResourceCollection.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/ResourceCollection.php b/src/Illuminate/Http/Resources/JsonApi/ResourceCollection.php index 9dfe6d6067f4..e56078614dc5 100644 --- a/src/Illuminate/Http/Resources/JsonApi/ResourceCollection.php +++ b/src/Illuminate/Http/Resources/JsonApi/ResourceCollection.php @@ -8,9 +8,12 @@ class ResourceCollection extends AnonymousResourceCollection { /** - * @param Request $request + * Get any additional data that should be returned with the resource array. + * + * @param \Illuminate\Http\Request $request * @return array{included?: array, jsonapi: ServerImplementation} */ + #[\Override] public function with($request) { return array_filter([ @@ -31,6 +34,7 @@ public function with($request) * @param \Illuminate\Http\Request $request * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable */ + #[\Override] public function toArray(Request $request) { return $this->collection From c742be0654d9b722321eeb45cbd24285bff6a0d6 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 12:50:46 +0800 Subject: [PATCH 26/74] wip Signed-off-by: Mior Muhammad Zaki --- .../Resources/JsonApi/JsonApiResource.php | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index 5cf981c01b2e..08b555ade07a 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -18,6 +18,22 @@ class JsonApiResource extends JsonResource */ public static $jsonApiInformation = []; + /** + * Set the JSON:API version for the request. + * + * @param string $version + * @return void + */ + public static function configure(?string $version = null, array $ext = [], array $profile = [], array $meta = []) + { + static::$jsonApiInformation = array_filter([ + 'version' => $version, + 'ext' => $ext, + 'profile' => $profile, + 'meta' => $meta, + ]); + } + public function links(Request $request) { return [ @@ -43,9 +59,12 @@ public function type(Request $request) } /** - * @param Request $request + * Get any additional data that should be returned with the resource array. + * + * @param \Illuminate\Http\Request $request * @return array{included?: array, jsonapi: ServerImplementation} */ + #[\Override] public function with($request) { return array_filter([ @@ -56,22 +75,6 @@ public function with($request) ]); } - /** - * Set the JSON:API version for the request. - * - * @param string $version - * @return void - */ - public static function configure(?string $version = null, array $ext = [], array $profile = [], array $meta = []) - { - static::$jsonApiInformation = array_filter([ - 'version' => $version, - 'ext' => $ext, - 'profile' => $profile, - 'meta' => $meta, - ]); - } - /** * Resolve the resource to an array. * @@ -100,6 +103,10 @@ public function toResponse($request) /** * Customize the outgoing response for the resource. + * + * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\JsonResponse $response + * @return void */ #[\Override] public function withResponse(Request $request, JsonResponse $response): void @@ -107,9 +114,10 @@ public function withResponse(Request $request, JsonResponse $response): void $response->header('Content-type', 'application/vnd.api+json'); } - /** - * {@inheritdoc} + * Transform JSON resource to JSON:API. + * + * @return $this */ #[\Override] public function asJsonApi() From bc8e87a0f550d59a14c2d0d1a4ef1d17a7d33cdf Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 12:52:47 +0800 Subject: [PATCH 27/74] wip Signed-off-by: Mior Muhammad Zaki --- .../UnknownRelationshipException.php | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 src/Illuminate/Http/Resources/JsonApi/Exceptions/UnknownRelationshipException.php diff --git a/src/Illuminate/Http/Resources/JsonApi/Exceptions/UnknownRelationshipException.php b/src/Illuminate/Http/Resources/JsonApi/Exceptions/UnknownRelationshipException.php deleted file mode 100644 index a2f1fa0ab60d..000000000000 --- a/src/Illuminate/Http/Resources/JsonApi/Exceptions/UnknownRelationshipException.php +++ /dev/null @@ -1,26 +0,0 @@ - Date: Thu, 30 Oct 2025 12:53:57 +0800 Subject: [PATCH 28/74] wip Signed-off-by: Mior Muhammad Zaki --- .../Concerns/ResolvesJsonApiSpecifications.php | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php index 346ba458bead..fe170a45754f 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php @@ -164,17 +164,6 @@ protected function resolveResourceType(Request $request): string throw new RuntimeException('Unable to determine "type"'); } - /** - * Get unique key for the resource. - * - * @param \Illuminate\Http\Request $request - * @return array{0: string, 1: string} - */ - public function uniqueResourceKey(Request $request): array - { - return [$this->resolveResourceType($request), $this->resolveResourceIdentifier($request)]; - } - /** * Resolves `links` object for the resource. * From f9bfd62138cac947899129b7bc19d2356c454720 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 12:55:40 +0800 Subject: [PATCH 29/74] wip Signed-off-by: Mior Muhammad Zaki --- .../JsonApi/Concerns/ResolvesJsonApiSpecifications.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php index fe170a45754f..7e63a356589a 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Http\Request; +use Illuminate\Http\Resources\JsonApi\Exceptions\ResourceIdentificationException; use Illuminate\Http\Resources\JsonApi\JsonApiResource; use Illuminate\Support\Collection; use Illuminate\Support\Str; @@ -144,7 +145,7 @@ protected function resolveResourceIdentifier(Request $request): string return static::getResourceIdFromEloquent($this->resource); } - throw new RuntimeException('Unable to determine "type"'); + throw ResourceIdentificationException::attemptingToDetermineIdFor($this); } /** @@ -161,7 +162,7 @@ protected function resolveResourceType(Request $request): string return static::getResourceTypeFromEloquent($this->resource); } - throw new RuntimeException('Unable to determine "type"'); + throw ResourceIdentificationException::attemptingToDetermineTypeFor($this); } /** From 62c56c063b3004a9bf0de18168962b33c97d190c Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 30 Oct 2025 04:55:53 +0000 Subject: [PATCH 30/74] Apply fixes from StyleCI --- .../Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php index 7e63a356589a..9dd7c12ad379 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php @@ -11,7 +11,6 @@ use Illuminate\Support\Collection; use Illuminate\Support\Str; use JsonSerializable; -use RuntimeException; use WeakMap; trait ResolvesJsonApiSpecifications From e9c68b04badce624f8241b753bb615d418d142a3 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 16:51:43 +0800 Subject: [PATCH 31/74] wip Signed-off-by: Mior Muhammad Zaki --- .../Http/Resources/JsonApi/AnonymousJsonApiResource.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php index 9beee43dc854..c34bc46b48ee 100644 --- a/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php @@ -11,8 +11,9 @@ class AnonymousJsonApiResource extends JsonApiResource * Create a new resource instance. * * @param mixed $resource + * @param \Illuminate\Http\Resources\Json\JsonResource $jsonResource */ - public function __construct($resource, protected JsonResource $source) + public function __construct($resource, protected JsonResource $jsonResource) { parent::__construct($resource); } @@ -25,6 +26,6 @@ public function __construct($resource, protected JsonResource $source) */ public function toArray(Request $request) { - return $this->source->toArray($request); + return $this->jsonResource->toArray($request); } } From f0f58d9f0ff074b1fd5f90c6da465b27fea2d94e Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 16:59:52 +0800 Subject: [PATCH 32/74] wip Signed-off-by: Mior Muhammad Zaki --- .../JsonApi/AnonymousJsonApiResource.php | 1 + .../ResolvesJsonApiSpecifications.php | 2 +- .../Resources/JsonApi/JsonApiResource.php | 25 +++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php index c34bc46b48ee..b3f5b58b4b6f 100644 --- a/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php @@ -24,6 +24,7 @@ public function __construct($resource, protected JsonResource $jsonResource) * @param \Illuminate\Http\Request $request * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable */ + #[\Override] public function toArray(Request $request) { return $this->jsonResource->toArray($request); diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php index 9dd7c12ad379..a5d00916f4ba 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php @@ -172,7 +172,7 @@ protected function resolveResourceType(Request $request): string */ protected function resolveResourceLinks(Request $request): array { - return []; + return $this->links($request); } /** diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index 08b555ade07a..089f2cdb87e0 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -34,6 +34,12 @@ public static function configure(?string $version = null, array $ext = [], array ]); } + /** + * Resource "links" for JSON:API + * + * @param \Illuminate\Http\Request $request + * @return array + */ public function links(Request $request) { return [ @@ -41,6 +47,12 @@ public function links(Request $request) ]; } + /** + * Resource "meta" for JSON:API + * + * @param \Illuminate\Http\Request $request + * @return array + */ public function meta(Request $request) { return [ @@ -48,11 +60,23 @@ public function meta(Request $request) ]; } + /** + * Resource "id" for JSON:API + * + * @param \Illuminate\Http\Request $request + * @return string + */ public function id(Request $request) { return $this->resolveResourceIdentifier($request); } + /** + * Resource "type" for JSON:API + * + * @param \Illuminate\Http\Request $request + * @return string + */ public function type(Request $request) { return $this->resolveResourceType($request); @@ -142,6 +166,7 @@ protected static function newCollection($resource) * * @return void */ + #[\Override] public static function flushState() { parent::flushState(); From e1212fbe7656267d0054e76bf428586a71789801 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 30 Oct 2025 09:00:10 +0000 Subject: [PATCH 33/74] Apply fixes from StyleCI --- src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index 089f2cdb87e0..9facb1f3dbc6 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -35,7 +35,7 @@ public static function configure(?string $version = null, array $ext = [], array } /** - * Resource "links" for JSON:API + * Resource "links" for JSON:API. * * @param \Illuminate\Http\Request $request * @return array @@ -48,7 +48,7 @@ public function links(Request $request) } /** - * Resource "meta" for JSON:API + * Resource "meta" for JSON:API. * * @param \Illuminate\Http\Request $request * @return array @@ -61,7 +61,7 @@ public function meta(Request $request) } /** - * Resource "id" for JSON:API + * Resource "id" for JSON:API. * * @param \Illuminate\Http\Request $request * @return string @@ -72,7 +72,7 @@ public function id(Request $request) } /** - * Resource "type" for JSON:API + * Resource "type" for JSON:API. * * @param \Illuminate\Http\Request $request * @return string From 1928b74dd161fce57cc41523b024c0fc03bb972a Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 17:06:00 +0800 Subject: [PATCH 34/74] wip Signed-off-by: Mior Muhammad Zaki --- .../JsonApi/Concerns/ResolvesJsonApiSpecifications.php | 6 +++--- .../JsonApi/Exceptions/ResourceIdentificationException.php | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php index a5d00916f4ba..058a78387cb2 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php @@ -91,9 +91,9 @@ protected function resolveResourceRelationships(Request $request): void } return transform( - [static::getResourceTypeFromEloquent($relation), static::getResourceIdFromEloquent($relation)], - function ($uniqueKey) { - $this->cachedLoadedRelationshipsMap[$relation] = $uniqueKey; + [static::getResourceTypeFromEloquent($relations), static::getResourceIdFromEloquent($relations)], + function ($uniqueKey) use ($relations) { + $this->cachedLoadedRelationshipsMap[$relations] = $uniqueKey; return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]]; } diff --git a/src/Illuminate/Http/Resources/JsonApi/Exceptions/ResourceIdentificationException.php b/src/Illuminate/Http/Resources/JsonApi/Exceptions/ResourceIdentificationException.php index 5603dc00522d..7fa44db31b5e 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Exceptions/ResourceIdentificationException.php +++ b/src/Illuminate/Http/Resources/JsonApi/Exceptions/ResourceIdentificationException.php @@ -2,6 +2,8 @@ namespace Illuminate\Http\Resources\JsonApi\Exceptions; +use RuntimeException; + class ResourceIdentificationException extends RuntimeException { /** From 4e27c4b6795cac15cef4769ddc326a6261a6198a Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 17:07:51 +0800 Subject: [PATCH 35/74] wip Signed-off-by: Mior Muhammad Zaki --- src/Illuminate/Http/Resources/JsonApi/ResourceCollection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/ResourceCollection.php b/src/Illuminate/Http/Resources/JsonApi/ResourceCollection.php index e56078614dc5..b45add40e1ab 100644 --- a/src/Illuminate/Http/Resources/JsonApi/ResourceCollection.php +++ b/src/Illuminate/Http/Resources/JsonApi/ResourceCollection.php @@ -11,7 +11,7 @@ class ResourceCollection extends AnonymousResourceCollection * Get any additional data that should be returned with the resource array. * * @param \Illuminate\Http\Request $request - * @return array{included?: array, jsonapi: ServerImplementation} + * @return array */ #[\Override] public function with($request) From 387e2a4019b9848787d641e75d15a4e2ffc2db84 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 17:08:16 +0800 Subject: [PATCH 36/74] wip Signed-off-by: Mior Muhammad Zaki --- src/Illuminate/Http/Resources/JsonApi/ResourceCollection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/ResourceCollection.php b/src/Illuminate/Http/Resources/JsonApi/ResourceCollection.php index b45add40e1ab..0a41f597b4ff 100644 --- a/src/Illuminate/Http/Resources/JsonApi/ResourceCollection.php +++ b/src/Illuminate/Http/Resources/JsonApi/ResourceCollection.php @@ -32,7 +32,7 @@ public function with($request) * Transform the resource into a JSON array. * * @param \Illuminate\Http\Request $request - * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + * @return array */ #[\Override] public function toArray(Request $request) From 73c243aa9ea8a21eb58a5a54e0635d7e8cd48564 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 17:12:03 +0800 Subject: [PATCH 37/74] wip Signed-off-by: Mior Muhammad Zaki --- .../ResolvesJsonApiSpecifications.php | 90 ++++++++++++------- 1 file changed, 59 insertions(+), 31 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php index 058a78387cb2..6c949a1ee76e 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php @@ -26,7 +26,7 @@ trait ResolvesJsonApiSpecifications protected array $cachedLoadedRelationshipsIdentifier = []; /** - * Resolves `attributes` for the resource. + * Resolves `attributes` for the resource's data object. * * @param \Illuminate\Http\Request $request * @return string|int @@ -50,6 +50,33 @@ protected function resolveResourceAttributes(Request $request): array return $this->filter($data); } + /** + * Resolves `relationships` for the resource's data object. + * + * @param \Illuminate\Http\Request $request + * @return string|int + * + * @throws \RuntimeException + */ + protected function resolveResourceRelationshipsIdentifiers(Request $request): array + { + if (! $this->resource instanceof Model) { + return []; + } + + $this->compileResourceRelationships($request); + + return [ + 'data' => $this->cachedLoadedRelationshipsIdentifier, + ]; + } + + /** + * Resolves `data` for the resource. + * + * @param \Illuminate\Http\Request $request + * @return array + */ public function resolveResourceData(Request $request): array { return [ @@ -64,7 +91,37 @@ public function resolveResourceData(Request $request): array ]; } - protected function resolveResourceRelationships(Request $request): void + /** + * Resolves `included` for the resource. + * + * @param \Illuminate\Http\Request $request + * @return array + */ + public function resolveResourceIncluded(Request $request): array + { + $this->compileResourceRelationships($request); + + $relations = new Collection(); + + foreach ($this->cachedLoadedRelationshipsMap as $relation => $uniqueKey) { + $resource = rescue(fn () => $relation->toResource(), new JsonApiResource($relation), false); + $relations->push([ + 'id' => $uniqueKey[1], + 'type' => $uniqueKey[0], + 'attributes' => $resource->asJsonApi()->toArray($request), + ]); + } + + return $relations->uniqueStrict(fn ($relation): array => [$relation['id'], $relation['type']])->all(); + } + + /** + * Compile resource relationships. + * + * @param \Illuminate\Http\Request $request + * @return void + */ + protected function compileResourceRelationships(Request $request): void { if ($this->cachedLoadedRelationships instanceof WeakMap) { return; @@ -101,35 +158,6 @@ function ($uniqueKey) use ($relations) { })->all(); } - protected function resolveResourceRelationshipsIdentifiers(Request $request): array - { - if (! $this->resource instanceof Model) { - return []; - } - - $this->resolveResourceRelationships($request); - - return [ - 'data' => $this->cachedLoadedRelationshipsIdentifier, - ]; - } - - public function resolveResourceIncluded(Request $request): array - { - $relations = new Collection(); - - foreach ($this->cachedLoadedRelationshipsMap as $relation => $uniqueKey) { - $resource = rescue(fn () => $relation->toResource(), new JsonApiResource($relation), false); - $relations->push([ - 'id' => $uniqueKey[1], - 'type' => $uniqueKey[0], - 'attributes' => $resource->asJsonApi()->toArray($request), - ]); - } - - return $relations->uniqueStrict(fn ($relation): array => [$relation['id'], $relation['type']])->all(); - } - /** * Resolves `id` for the resource. * From ab05c9f3daf19430885aae1e05952ef9aaf8e8f6 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 17:27:23 +0800 Subject: [PATCH 38/74] wip Signed-off-by: Mior Muhammad Zaki --- .../Http/Resources/Json/JsonResource.php | 4 +-- .../JsonApi/AnonymousJsonApiResource.php | 32 +++++++++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Http/Resources/Json/JsonResource.php b/src/Illuminate/Http/Resources/Json/JsonResource.php index af2d4bb0f5d2..84dd4dd482ea 100644 --- a/src/Illuminate/Http/Resources/Json/JsonResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonResource.php @@ -280,9 +280,9 @@ public function jsonSerialize(): array * * @return \Illuminate\Http\Resources\JsonApi\AnonymousJsonApiResource */ - public function asJsonApi() + public function asJsonApi(array $links = [], array $meta = []) { - return new AnonymousJsonApiResource($this->resource, $this); + return new AnonymousJsonApiResource($this->resource, $this, $links, $meta); } /** diff --git a/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php index b3f5b58b4b6f..f5ab898e03e3 100644 --- a/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php @@ -13,11 +13,39 @@ class AnonymousJsonApiResource extends JsonApiResource * @param mixed $resource * @param \Illuminate\Http\Resources\Json\JsonResource $jsonResource */ - public function __construct($resource, protected JsonResource $jsonResource) - { + public function __construct( + $resource, + protected JsonResource $jsonResource, + protected array $jsonApiLinks = [], + protected array $jsonApiMeta = [], + ) { parent::__construct($resource); } + /** + * Resource "links" for JSON:API. + * + * @param \Illuminate\Http\Request $request + * @return array + */ + #[\Override] + public function links(Request $request) + { + return $this->jsonApiLinks; + } + + /** + * Resource "meta" for JSON:API. + * + * @param \Illuminate\Http\Request $request + * @return array + */ + #[\Override] + public function meta(Request $request) + { + return $this->jsonApiMeta; + } + /** * Transform the resource into an array. * From d8ecdbcd57adcdabe31cc013ec0c8cd54fb33362 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 18:08:56 +0800 Subject: [PATCH 39/74] wip Signed-off-by: Mior Muhammad Zaki --- .../Http/Resources/Json/JsonResource.php | 2 ++ .../JsonApi/AnonymousJsonApiResource.php | 29 +++------------ .../Resources/JsonApi/JsonApiResource.php | 36 ++++++++++++++----- .../JsonApi/ResourceResponseTest.php | 26 ++++++++++++++ 4 files changed, 60 insertions(+), 33 deletions(-) create mode 100644 tests/Http/Resources/JsonApi/ResourceResponseTest.php diff --git a/src/Illuminate/Http/Resources/Json/JsonResource.php b/src/Illuminate/Http/Resources/Json/JsonResource.php index 84dd4dd482ea..4fee8061f13d 100644 --- a/src/Illuminate/Http/Resources/Json/JsonResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonResource.php @@ -278,6 +278,8 @@ public function jsonSerialize(): array /** * Transform JSON resource to JSON:API. * + * @param array $links + * @param array $meta * @return \Illuminate\Http\Resources\JsonApi\AnonymousJsonApiResource */ public function asJsonApi(array $links = [], array $meta = []) diff --git a/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php index f5ab898e03e3..aeab536ce67b 100644 --- a/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php @@ -16,34 +16,13 @@ class AnonymousJsonApiResource extends JsonApiResource public function __construct( $resource, protected JsonResource $jsonResource, - protected array $jsonApiLinks = [], - protected array $jsonApiMeta = [], + array $links = [], + array $meta = [], ) { parent::__construct($resource); - } - /** - * Resource "links" for JSON:API. - * - * @param \Illuminate\Http\Request $request - * @return array - */ - #[\Override] - public function links(Request $request) - { - return $this->jsonApiLinks; - } - - /** - * Resource "meta" for JSON:API. - * - * @param \Illuminate\Http\Request $request - * @return array - */ - #[\Override] - public function meta(Request $request) - { - return $this->jsonApiMeta; + $this->jsonApiLinks = $links; + $this->jsonApiMeta = $meta; } /** diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index 9facb1f3dbc6..93ac75ed92a8 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -18,6 +18,20 @@ class JsonApiResource extends JsonResource */ public static $jsonApiInformation = []; + /** + * The resource's "links" for JSON:API. + * + * @var array + */ + protected array $jsonApiLinks = []; + + /** + * The resource's "meta" for JSON:API. + * + * @var array + */ + protected array $jsonApiMeta = []; + /** * Set the JSON:API version for the request. * @@ -42,9 +56,7 @@ public static function configure(?string $version = null, array $ext = [], array */ public function links(Request $request) { - return [ - // - ]; + return $this->jsonApiLinks; } /** @@ -55,9 +67,7 @@ public function links(Request $request) */ public function meta(Request $request) { - return [ - // - ]; + return $this->jsonApiMeta; } /** @@ -138,14 +148,24 @@ public function withResponse(Request $request, JsonResponse $response): void $response->header('Content-type', 'application/vnd.api+json'); } + /** * Transform JSON resource to JSON:API. * + * @param array $links + * @param array $meta * @return $this */ - #[\Override] - public function asJsonApi() + public function asJsonApi(array $links = [], array $meta = []) { + if (! empty($links)) { + $this->jsonApiLinks = array_merge($this->jsonApiLinks, $links); + } + + if (! empty($meta)) { + $this->jsonApiMeta = array_merge($this->jsonApiMeta, $meta); + } + return $this; } diff --git a/tests/Http/Resources/JsonApi/ResourceResponseTest.php b/tests/Http/Resources/JsonApi/ResourceResponseTest.php new file mode 100644 index 000000000000..f250c7b7e955 --- /dev/null +++ b/tests/Http/Resources/JsonApi/ResourceResponseTest.php @@ -0,0 +1,26 @@ +assertSame('data', (new class([]) extends ResourceResponse { + public function getWrapper() { + return $this->wrapper(); + } + })->getWrapper()); + } +} From 9ce5b0742dc2923ad1bec6df8fd3aa7ae03c5e33 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 30 Oct 2025 10:09:11 +0000 Subject: [PATCH 40/74] Apply fixes from StyleCI --- src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php | 1 - tests/Http/Resources/JsonApi/ResourceResponseTest.php | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index 93ac75ed92a8..acec81658de5 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -148,7 +148,6 @@ public function withResponse(Request $request, JsonResponse $response): void $response->header('Content-type', 'application/vnd.api+json'); } - /** * Transform JSON resource to JSON:API. * diff --git a/tests/Http/Resources/JsonApi/ResourceResponseTest.php b/tests/Http/Resources/JsonApi/ResourceResponseTest.php index f250c7b7e955..4040b5635bd4 100644 --- a/tests/Http/Resources/JsonApi/ResourceResponseTest.php +++ b/tests/Http/Resources/JsonApi/ResourceResponseTest.php @@ -17,8 +17,10 @@ public function testResponseWrapperIsHardCodedToData() { JsonApiResource::wrap('laravel'); - $this->assertSame('data', (new class([]) extends ResourceResponse { - public function getWrapper() { + $this->assertSame('data', (new class([]) extends ResourceResponse + { + public function getWrapper() + { return $this->wrapper(); } })->getWrapper()); From e1f0bbdeebb10c172b3111f956efd3cff3ee269c Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 18:21:25 +0800 Subject: [PATCH 41/74] wip Signed-off-by: Mior Muhammad Zaki --- .../Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php index 6c949a1ee76e..3416380afa57 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php @@ -105,6 +105,7 @@ public function resolveResourceIncluded(Request $request): array foreach ($this->cachedLoadedRelationshipsMap as $relation => $uniqueKey) { $resource = rescue(fn () => $relation->toResource(), new JsonApiResource($relation), false); + $relations->push([ 'id' => $uniqueKey[1], 'type' => $uniqueKey[0], From 5b3f81c525dcc94f25cc989d92f9082d570b356c Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 18:42:19 +0800 Subject: [PATCH 42/74] wip Signed-off-by: Mior Muhammad Zaki --- .../Json/AnonymousResourceCollection.php | 17 +++++++++++++++++ .../Http/Resources/Json/JsonResource.php | 2 +- .../JsonApi/AnonymousJsonApiResource.php | 3 +-- ...tion.php => AnonymousResourceCollection.php} | 4 ++-- .../Concerns/ResolvesJsonApiSpecifications.php | 2 +- .../Http/Resources/JsonApi/JsonApiResource.php | 4 ++-- 6 files changed, 24 insertions(+), 8 deletions(-) rename src/Illuminate/Http/Resources/JsonApi/{ResourceCollection.php => AnonymousResourceCollection.php} (90%) diff --git a/src/Illuminate/Http/Resources/Json/AnonymousResourceCollection.php b/src/Illuminate/Http/Resources/Json/AnonymousResourceCollection.php index ba8c087f1194..c53c53270ad5 100644 --- a/src/Illuminate/Http/Resources/Json/AnonymousResourceCollection.php +++ b/src/Illuminate/Http/Resources/Json/AnonymousResourceCollection.php @@ -2,6 +2,8 @@ namespace Illuminate\Http\Resources\Json; +use Illuminate\Http\Resources\JsonApi\AnonymousResourceCollection as JsonApiAnonymousResourceCollection; + class AnonymousResourceCollection extends ResourceCollection { /** @@ -30,4 +32,19 @@ public function __construct($resource, $collects) parent::__construct($resource); } + + /** + * Transform JSON resource to JSON:API. + * + * @param array $links + * @param array $meta + * @return $this + */ + public function asJsonApi(array $links = [], array $meta = []) + { + return new JsonApiAnonymousResourceCollection( + $this->collection->map(fn ($resource) => $resource->asJsonApi($links, $meta)), + $this->collects + ); + } } diff --git a/src/Illuminate/Http/Resources/Json/JsonResource.php b/src/Illuminate/Http/Resources/Json/JsonResource.php index 4fee8061f13d..209ced023591 100644 --- a/src/Illuminate/Http/Resources/Json/JsonResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonResource.php @@ -284,7 +284,7 @@ public function jsonSerialize(): array */ public function asJsonApi(array $links = [], array $meta = []) { - return new AnonymousJsonApiResource($this->resource, $this, $links, $meta); + return new AnonymousJsonApiResource($this, $links, $meta); } /** diff --git a/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php index aeab536ce67b..d67afa2103f8 100644 --- a/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php @@ -14,12 +14,11 @@ class AnonymousJsonApiResource extends JsonApiResource * @param \Illuminate\Http\Resources\Json\JsonResource $jsonResource */ public function __construct( - $resource, protected JsonResource $jsonResource, array $links = [], array $meta = [], ) { - parent::__construct($resource); + parent::__construct($jsonResource->resource); $this->jsonApiLinks = $links; $this->jsonApiMeta = $meta; diff --git a/src/Illuminate/Http/Resources/JsonApi/ResourceCollection.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php similarity index 90% rename from src/Illuminate/Http/Resources/JsonApi/ResourceCollection.php rename to src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php index 0a41f597b4ff..2c10efe38259 100644 --- a/src/Illuminate/Http/Resources/JsonApi/ResourceCollection.php +++ b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php @@ -3,9 +3,8 @@ namespace Illuminate\Http\Resources\JsonApi; use Illuminate\Http\Request; -use Illuminate\Http\Resources\Json\AnonymousResourceCollection; -class ResourceCollection extends AnonymousResourceCollection +class AnonymousResourceCollection extends \Illuminate\Http\Resources\Json\AnonymousResourceCollection { /** * Get any additional data that should be returned with the resource array. @@ -21,6 +20,7 @@ public function with($request) ->map(fn ($resource) => $resource->resolveResourceIncluded($request)) ->flatten(depth: 1) ->uniqueStrict(fn ($relation): array => [$relation['id'], $relation['type']]) + ->dump() ->all(), ...($implementation = JsonApiResource::$jsonApiInformation) ? ['jsonapi' => $implementation] diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php index 3416380afa57..689b6d9ad675 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php @@ -124,7 +124,7 @@ public function resolveResourceIncluded(Request $request): array */ protected function compileResourceRelationships(Request $request): void { - if ($this->cachedLoadedRelationships instanceof WeakMap) { + if ($this->cachedLoadedRelationshipsMap instanceof WeakMap) { return; } diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index acec81658de5..abd5ecdcfdf4 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -172,12 +172,12 @@ public function asJsonApi(array $links = [], array $meta = []) * Create a new resource collection instance. * * @param mixed $resource - * @return \Illuminate\Http\Resources\JsonApi\JsonApiResourceCollection + * @return \Illuminate\Http\Resources\JsonApi\AnonymousResourceCollection */ #[\Override] protected static function newCollection($resource) { - return new ResourceCollection($resource, static::class); + return new AnonymousResourceCollection($resource, static::class); } /** From eda7c7619e122b22072e990516e5fc5f3e93b787 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 18:43:11 +0800 Subject: [PATCH 43/74] wip Signed-off-by: Mior Muhammad Zaki --- .../Http/Resources/Json/AnonymousResourceCollection.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Http/Resources/Json/AnonymousResourceCollection.php b/src/Illuminate/Http/Resources/Json/AnonymousResourceCollection.php index c53c53270ad5..b3c10dfbe679 100644 --- a/src/Illuminate/Http/Resources/Json/AnonymousResourceCollection.php +++ b/src/Illuminate/Http/Resources/Json/AnonymousResourceCollection.php @@ -38,13 +38,13 @@ public function __construct($resource, $collects) * * @param array $links * @param array $meta - * @return $this + * @return Illuminate\Http\Resources\JsonApi\AnonymousResourceCollection */ public function asJsonApi(array $links = [], array $meta = []) { return new JsonApiAnonymousResourceCollection( $this->collection->map(fn ($resource) => $resource->asJsonApi($links, $meta)), - $this->collects + $this->collects, ); } } From 9b279bb8c76a18c3118ead27e0dc9edfee062fa8 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 18:44:55 +0800 Subject: [PATCH 44/74] Apply suggestions from code review --- src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index abd5ecdcfdf4..70846fa098e8 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -96,7 +96,7 @@ public function type(Request $request) * Get any additional data that should be returned with the resource array. * * @param \Illuminate\Http\Request $request - * @return array{included?: array, jsonapi: ServerImplementation} + * @return array */ #[\Override] public function with($request) @@ -172,7 +172,7 @@ public function asJsonApi(array $links = [], array $meta = []) * Create a new resource collection instance. * * @param mixed $resource - * @return \Illuminate\Http\Resources\JsonApi\AnonymousResourceCollection + * @return \Illuminate\Http\Resources\JsonApi\AnonymousResourceCollection */ #[\Override] protected static function newCollection($resource) From 6275377f89a995dc962387d179ee4f03b49e444d Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 18:49:09 +0800 Subject: [PATCH 45/74] wip Signed-off-by: Mior Muhammad Zaki --- .../Http/Resources/JsonApi/AnonymousJsonApiResource.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php index d67afa2103f8..18b5457c5a31 100644 --- a/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php @@ -10,8 +10,9 @@ class AnonymousJsonApiResource extends JsonApiResource /** * Create a new resource instance. * - * @param mixed $resource * @param \Illuminate\Http\Resources\Json\JsonResource $jsonResource + * @param array $links + * @param array $meta */ public function __construct( protected JsonResource $jsonResource, From 280c18c55555330f99b98f0345270c3aea6f21eb Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 19:06:05 +0800 Subject: [PATCH 46/74] wip Signed-off-by: Mior Muhammad Zaki --- .../JsonApi/AnonymousResourceCollection.php | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php index 2c10efe38259..fb7c4477a0d2 100644 --- a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php +++ b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php @@ -3,6 +3,8 @@ namespace Illuminate\Http\Resources\JsonApi; use Illuminate\Http\Request; +use Illuminate\Pagination\AbstractCursorPaginator; +use Illuminate\Pagination\AbstractPaginator; class AnonymousResourceCollection extends \Illuminate\Http\Resources\Json\AnonymousResourceCollection { @@ -41,4 +43,33 @@ public function toArray(Request $request) ->map(fn ($resource) => $resource->resolveResourceData($request)) ->all(); } + + /** + * Create an HTTP response that represents the object. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + #[\Override] + public function toResponse($request) + { + if ($this->resource instanceof AbstractPaginator || $this->resource instanceof AbstractCursorPaginator) { + return $this->preparePaginatedResponse($request); + } + + return (new ResourceResponse($this))->toResponse($request); + } + + /** + * Customize the outgoing response for the resource. + * + * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\JsonResponse $response + * @return void + */ + #[\Override] + public function withResponse(Request $request, JsonResponse $response): void + { + $response->header('Content-type', 'application/vnd.api+json'); + } } From ac0ec25b3b3abc9ca20e8a5ffe83e19603dcebde Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 19:10:46 +0800 Subject: [PATCH 47/74] wip Signed-off-by: Mior Muhammad Zaki --- .../JsonApi/Concerns/ResolvesJsonApiSpecifications.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php index 689b6d9ad675..a607168a9e37 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php @@ -16,11 +16,14 @@ trait ResolvesJsonApiSpecifications { /** + * Cached loaded relationships map. + * * @var \WeakMap|null */ protected $cachedLoadedRelationshipsMap; /** + * Cached loaded relationships identifers. * @var array */ protected array $cachedLoadedRelationshipsIdentifier = []; From f46656678f47e0623eb4f57652849e83666c752e Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 30 Oct 2025 11:10:59 +0000 Subject: [PATCH 48/74] Apply fixes from StyleCI --- .../Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php index a607168a9e37..364b6d0a8555 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php @@ -24,6 +24,7 @@ trait ResolvesJsonApiSpecifications /** * Cached loaded relationships identifers. + * * @var array */ protected array $cachedLoadedRelationshipsIdentifier = []; From d45e57b44f9dfefa75a09ae24fefb8cd557053f8 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 19:11:30 +0800 Subject: [PATCH 49/74] wip Signed-off-by: Mior Muhammad Zaki --- .../Http/Resources/JsonApi/AnonymousResourceCollection.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php index fb7c4477a0d2..f8cbf3645d63 100644 --- a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php +++ b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php @@ -2,6 +2,7 @@ namespace Illuminate\Http\Resources\JsonApi; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Pagination\AbstractCursorPaginator; use Illuminate\Pagination\AbstractPaginator; From dc0ec651ba75ed3c572a0c950d9026d568648d37 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 19:36:40 +0800 Subject: [PATCH 50/74] wip Signed-off-by: Mior Muhammad Zaki --- .../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 f8cbf3645d63..87178d1eaeb0 100644 --- a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php +++ b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php @@ -23,7 +23,6 @@ public function with($request) ->map(fn ($resource) => $resource->resolveResourceIncluded($request)) ->flatten(depth: 1) ->uniqueStrict(fn ($relation): array => [$relation['id'], $relation['type']]) - ->dump() ->all(), ...($implementation = JsonApiResource::$jsonApiInformation) ? ['jsonapi' => $implementation] From f4bf0308569aa04d75c4a3fa928451c193fc0c06 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 19:49:27 +0800 Subject: [PATCH 51/74] wip Signed-off-by: Mior Muhammad Zaki --- .../JsonApi/AnonymousResourceCollection.php | 16 ------- .../Resources/JsonApi/JsonApiResource.php | 46 ++++++++++++++----- .../Resources/JsonApi/ResourceResponse.php | 17 ------- .../Resources/JsonApi/JsonApiResourceTest.php | 23 ++++++++++ .../JsonApi/ResourceResponseTest.php | 28 ----------- 5 files changed, 57 insertions(+), 73 deletions(-) delete mode 100644 src/Illuminate/Http/Resources/JsonApi/ResourceResponse.php create mode 100644 tests/Http/Resources/JsonApi/JsonApiResourceTest.php delete mode 100644 tests/Http/Resources/JsonApi/ResourceResponseTest.php diff --git a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php index 87178d1eaeb0..dec522a2edc2 100644 --- a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php +++ b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php @@ -44,22 +44,6 @@ public function toArray(Request $request) ->all(); } - /** - * Create an HTTP response that represents the object. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\JsonResponse - */ - #[\Override] - public function toResponse($request) - { - if ($this->resource instanceof AbstractPaginator || $this->resource instanceof AbstractCursorPaginator) { - return $this->preparePaginatedResponse($request); - } - - return (new ResourceResponse($this))->toResponse($request); - } - /** * Customize the outgoing response for the resource. * diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index 70846fa098e8..cb8e27328d38 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -2,6 +2,7 @@ namespace Illuminate\Http\Resources\JsonApi; +use BadMethodCallException; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; @@ -11,6 +12,13 @@ class JsonApiResource extends JsonResource { use Concerns\ResolvesJsonApiSpecifications; + /** + * The "data" wrapper that should be applied. + * + * @var string|null + */ + public static $wrap = 'data'; + /** * The resource's "version" for JSON:API. * @@ -48,6 +56,32 @@ public static function configure(?string $version = null, array $ext = [], array ]); } + + /** + * Set the string that should wrap the outer-most resource array. + * + * @param string $value + * @return never + * + * @throws \RuntimeException + */ + #[\Override] + public static function wrap($value) + { + throw new BadMethodCallException(sprintf('Using %s() method is not allowed.', __METHOD__); + } + + /** + * Disable wrapping of the outer-most resource array. + * + * @return never + */ + #[\Override] + public static function withoutWrapping() + { + throw new BadMethodCallException(sprintf('Using %s() method is not allowed.', __METHOD__); + } + /** * Resource "links" for JSON:API. * @@ -123,18 +157,6 @@ public function resolve($request = null) ]; } - /** - * Create an HTTP response that represents the object. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\JsonResponse - */ - #[\Override] - public function toResponse($request) - { - return (new ResourceResponse($this))->toResponse($request); - } - /** * Customize the outgoing response for the resource. * diff --git a/src/Illuminate/Http/Resources/JsonApi/ResourceResponse.php b/src/Illuminate/Http/Resources/JsonApi/ResourceResponse.php deleted file mode 100644 index 3fdbcc935336..000000000000 --- a/src/Illuminate/Http/Resources/JsonApi/ResourceResponse.php +++ /dev/null @@ -1,17 +0,0 @@ -assertSame('data', JsonApiResource::$wrap); + } +} diff --git a/tests/Http/Resources/JsonApi/ResourceResponseTest.php b/tests/Http/Resources/JsonApi/ResourceResponseTest.php deleted file mode 100644 index 4040b5635bd4..000000000000 --- a/tests/Http/Resources/JsonApi/ResourceResponseTest.php +++ /dev/null @@ -1,28 +0,0 @@ -assertSame('data', (new class([]) extends ResourceResponse - { - public function getWrapper() - { - return $this->wrapper(); - } - })->getWrapper()); - } -} From e5c2f8818dd190404c2884f58e1fd39e6c7b518a Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 30 Oct 2025 11:49:47 +0000 Subject: [PATCH 52/74] Apply fixes from StyleCI --- .../Http/Resources/JsonApi/AnonymousResourceCollection.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php index dec522a2edc2..e2f54ddb0880 100644 --- a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php +++ b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php @@ -4,8 +4,6 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Pagination\AbstractCursorPaginator; -use Illuminate\Pagination\AbstractPaginator; class AnonymousResourceCollection extends \Illuminate\Http\Resources\Json\AnonymousResourceCollection { From 4a8cfc212f0e26564a91fb2b3d99e9b1143141c7 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 19:52:14 +0800 Subject: [PATCH 53/74] wip Signed-off-by: Mior Muhammad Zaki --- .../Http/Resources/JsonApi/JsonApiResource.php | 4 ++-- .../Resources/JsonApi/JsonApiResourceTest.php | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index cb8e27328d38..52891871eea9 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -68,7 +68,7 @@ public static function configure(?string $version = null, array $ext = [], array #[\Override] public static function wrap($value) { - throw new BadMethodCallException(sprintf('Using %s() method is not allowed.', __METHOD__); + throw new BadMethodCallException(sprintf('Using %s() method is not allowed.', __METHOD__)); } /** @@ -79,7 +79,7 @@ public static function wrap($value) #[\Override] public static function withoutWrapping() { - throw new BadMethodCallException(sprintf('Using %s() method is not allowed.', __METHOD__); + throw new BadMethodCallException(sprintf('Using %s() method is not allowed.', __METHOD__)); } /** diff --git a/tests/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Http/Resources/JsonApi/JsonApiResourceTest.php index 319eb4c783ea..9f3fc8a94d6f 100644 --- a/tests/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Http\Resources\JsonApi; +use BadMethodCallException; use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\JsonApi\JsonApiResource; use PHPUnit\Framework\TestCase; @@ -20,4 +21,20 @@ public function testResponseWrapperIsHardCodedToData() $this->assertSame('data', JsonApiResource::$wrap); } + + public function testUnableToSetWrapper() + { + $this->expectException(BadMethodCallException::class); + $this->expectExceptionMessage('Using Illuminate\Http\Resources\JsonApi\JsonApiResource::wrap() method is not allowed.'); + + JsonApiResource::wrap('laravel'); + } + + public function testUnableToUnsetWrapper() + { + $this->expectException(BadMethodCallException::class); + $this->expectExceptionMessage('Using Illuminate\Http\Resources\JsonApi\JsonApiResource::withoutWrapping() method is not allowed.'); + + JsonApiResource::withoutWrapping(); + } } From 8f9d1511dffcda6ea0328ffcdb007536650aa1fb Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 30 Oct 2025 11:52:56 +0000 Subject: [PATCH 54/74] Apply fixes from StyleCI --- src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index 52891871eea9..cba19b2ce9f0 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -56,7 +56,6 @@ public static function configure(?string $version = null, array $ext = [], array ]); } - /** * Set the string that should wrap the outer-most resource array. * From 76cc84e0c9443de9df26368581722ec7f92cc093 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 21:03:22 +0800 Subject: [PATCH 55/74] wip Signed-off-by: Mior Muhammad Zaki --- .../Resources/JsonApi/JsonApiResourceTest.php | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php new file mode 100644 index 000000000000..53efb60f4c17 --- /dev/null +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -0,0 +1,63 @@ +get('users/{userId}', function (Request $request, $userId) { + return new UserResource(User::find($userId)); + }); + } + + public function testItCanGenerateJsonApiResponse() + { + $user = UserFactory::new()->create(); + + $this->getJson('/users/'.$user->getKey()) + ->assertHeader('Content-type', 'application/vnd.api+json') + ->assertJson([ + 'data' => [ + 'id' => (string) $user->getKey(), + 'type' => 'users', + 'attributes' => [ + 'name' => $user->name, + 'email' => $user->email, + ], + 'relationships' => [ + 'data' => [], + ], + ], + ]); + } +} + +class User extends Authenticatable +{ + +} + +class UserResource extends JsonApiResource +{ + public function toArray(Request $request) + { + return [ + 'name' => $this->name, + 'email' => $this->email, + ]; + } +} From 86884a92586f1f045daa6c8325949398688031bf Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 30 Oct 2025 13:04:15 +0000 Subject: [PATCH 56/74] Apply fixes from StyleCI --- tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index 53efb60f4c17..22cf82e7d056 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -48,7 +48,6 @@ public function testItCanGenerateJsonApiResponse() class User extends Authenticatable { - } class UserResource extends JsonApiResource From afb51d17ba72a3ba9917f6b0f6dafc3bc078922c Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 21:08:26 +0800 Subject: [PATCH 57/74] wip Signed-off-by: Mior Muhammad Zaki --- .../Resources/JsonApi/JsonApiResourceTest.php | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index 22cf82e7d056..3f49f6085c1f 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -6,6 +6,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\Request; use Illuminate\Http\Resources\JsonApi\JsonApiResource; +use Illuminate\Http\Resources\Json\JsonResource; use Orchestra\Testbench\Attributes\WithMigration; use Orchestra\Testbench\Factories\UserFactory; use Orchestra\Testbench\TestCase; @@ -20,10 +21,19 @@ class JsonApiResourceTest extends TestCase protected function defineRoutes($router) { $router->get('users/{userId}', function (Request $request, $userId) { - return new UserResource(User::find($userId)); + return new UserApiResource(User::find($userId)); }); } + public function testBaseJsonResourceCanBeConvertedToJsonApiResource() + { + $user = UserFactory::new()->create(); + + $resource = (new UserResource($user))->asJsonApi(); + + $this->assertInstanceOf(JsonApiResource::class, $resource); + } + public function testItCanGenerateJsonApiResponse() { $user = UserFactory::new()->create(); @@ -48,9 +58,21 @@ public function testItCanGenerateJsonApiResponse() class User extends Authenticatable { + // +} + +class UserResource extends JsonResource +{ + public function toArray(Request $request) + { + return [ + 'name' => $this->name, + 'email' => $this->email, + ]; + } } -class UserResource extends JsonApiResource +class UserApiResource extends JsonApiResource { public function toArray(Request $request) { From 5f52c26aec67c359b4f53b4f4f0bf8618c968f8a Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 30 Oct 2025 13:10:25 +0000 Subject: [PATCH 58/74] 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 3f49f6085c1f..e864fd74eddd 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -5,8 +5,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 Orchestra\Testbench\Attributes\WithMigration; use Orchestra\Testbench\Factories\UserFactory; use Orchestra\Testbench\TestCase; From 428461941fe8b4c4eb6e46fb08ed1192f4f0bb35 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 22:05:39 +0800 Subject: [PATCH 59/74] wip Signed-off-by: Mior Muhammad Zaki --- .../ResolvesJsonApiSpecifications.php | 12 +- .../Resources/JsonApi/JsonApiResourceTest.php | 114 +++++++++++++++++- 2 files changed, 116 insertions(+), 10 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php index 364b6d0a8555..4aa094119a5d 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php @@ -71,7 +71,7 @@ protected function resolveResourceRelationshipsIdentifiers(Request $request): ar $this->compileResourceRelationships($request); return [ - 'data' => $this->cachedLoadedRelationshipsIdentifier, + ...$this->cachedLoadedRelationshipsIdentifier, ]; } @@ -138,28 +138,28 @@ protected function compileResourceRelationships(Request $request): void ->mapWithKeys(function ($relations, $key) { if ($relations instanceof Collection) { if ($relations->isEmpty()) { - return [$key => $relations]; + return [$key => ['data' => $relations]]; } $key = static::getResourceTypeFromEloquent($relations->first()); - return [$key => $relations->map(function ($relation) use ($key) { + return [$key => ['data' => $relations->map(function ($relation) use ($key) { return transform([$key, static::getResourceIdFromEloquent($relation)], function ($uniqueKey) use ($relation) { $this->cachedLoadedRelationshipsMap[$relation] = $uniqueKey; return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]]; }); - })]; + })]]; } - return transform( + return [$key => ['data' => transform( [static::getResourceTypeFromEloquent($relations), static::getResourceIdFromEloquent($relations)], function ($uniqueKey) use ($relations) { $this->cachedLoadedRelationshipsMap[$relations] = $uniqueKey; return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]]; } - ); + )]]; })->all(); } diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index e864fd74eddd..f1dd029d140e 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -2,11 +2,16 @@ namespace Illuminate\Tests\Integration\Http\Resources\JsonApi; +use Illuminate\Database\Eloquent\Attributes\UseResource; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Factories\Factory; +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\WithMigration; use Orchestra\Testbench\Factories\UserFactory; use Orchestra\Testbench\TestCase; @@ -21,7 +26,25 @@ class JsonApiResourceTest extends TestCase protected function defineRoutes($router) { $router->get('users/{userId}', function (Request $request, $userId) { - return new UserApiResource(User::find($userId)); + $user = User::find($userId); + + if (! empty($includes = $request->array('includes'))) { + $user->loadMissing($includes); + } + + return $user->toResource(); + }); + } + + /** {@inheritdoc} */ + protected function afterRefreshingDatabase() + { + Schema::create('posts', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id'); + $table->string('title'); + $table->text('content'); + $table->timestamps(); }); } @@ -40,7 +63,30 @@ public function testItCanGenerateJsonApiResponse() $this->getJson('/users/'.$user->getKey()) ->assertHeader('Content-type', 'application/vnd.api+json') - ->assertJson([ + ->assertExactJson([ + 'data' => [ + 'id' => (string) $user->getKey(), + 'type' => 'users', + 'attributes' => [ + 'name' => $user->name, + 'email' => $user->email, + ], + ], + ]) + ->assertJsonMissing(['jsonapi', 'included']); + } + + public function testItCanGenerateJsonApiResponseWithEagerLoadedRelationship() + { + $user = UserFactory::new()->create(); + + $posts = PostFactory::new()->times(2)->create([ + 'user_id' => $user->getKey(), + ]); + + $this->getJson('/users/'.$user->getKey().'?'.http_build_query(['includes' => ['posts']])) + ->assertHeader('Content-type', 'application/vnd.api+json') + ->assertExactJson([ 'data' => [ 'id' => (string) $user->getKey(), 'type' => 'users', @@ -49,16 +95,37 @@ public function testItCanGenerateJsonApiResponse() 'email' => $user->email, ], 'relationships' => [ - 'data' => [], + 'posts' => [ + 'data' => [ + ['id' => (string) $posts[0]->getKey(), 'type' => 'posts'], + ['id' => (string) $posts[1]->getKey(), 'type' => 'posts'], + ] + ], + ], + ], + 'included' => [ + [ + '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], ], ], ]); } } +#[UseResource(UserApiResource::class)] class User extends Authenticatable { - // + public function posts() + { + return $this->hasMany(Post::class); + } } class UserResource extends JsonResource @@ -82,3 +149,42 @@ public function toArray(Request $request) ]; } } + + +#[UseResource(PostApiResource::class)] +class Post extends Model +{ + public function user() + { + return $this->belongsTo(User::class); + } +} + +class PostApiResource extends JsonApiResource +{ + public function toArray(Request $request) + { + return [ + 'title' => $this->title, + 'content' => $this->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; + } +} From 504193d2db3b09e6339847ba1e43ba853351439c Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 30 Oct 2025 14:07:02 +0000 Subject: [PATCH 60/74] Apply fixes from StyleCI --- .../Http/Resources/JsonApi/JsonApiResourceTest.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index f1dd029d140e..d0882653a9d9 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -3,8 +3,8 @@ namespace Illuminate\Tests\Integration\Http\Resources\JsonApi; use Illuminate\Database\Eloquent\Attributes\UseResource; -use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Schema\Blueprint; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -99,7 +99,7 @@ public function testItCanGenerateJsonApiResponseWithEagerLoadedRelationship() 'data' => [ ['id' => (string) $posts[0]->getKey(), 'type' => 'posts'], ['id' => (string) $posts[1]->getKey(), 'type' => 'posts'], - ] + ], ], ], ], @@ -150,7 +150,6 @@ public function toArray(Request $request) } } - #[UseResource(PostApiResource::class)] class Post extends Model { From 1a51b540a8d2f0adf5858d66e20abf103e518df9 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 22:09:26 +0800 Subject: [PATCH 61/74] wip Signed-off-by: Mior Muhammad Zaki --- .../Http/Resources/JsonApi/JsonApiResourceTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index d0882653a9d9..552041f6b9b4 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -21,6 +21,16 @@ class JsonApiResourceTest extends TestCase { use RefreshDatabase; + /** {@inheritdoc} */ + #[\Override] + protected function tearDown(): void + { + parent::tearDown(); + + JsonResource::flushState(); + JsonApiResource::flushState(); + } + /** {@inheritdoc} */ #[\Override] protected function defineRoutes($router) From 0ed469a560506fdfb0dccc92a724253a7f382f3d Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Oct 2025 22:16:03 +0800 Subject: [PATCH 62/74] wip Signed-off-by: Mior Muhammad Zaki --- .../Resources/JsonApi/JsonApiResourceTest.php | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index 552041f6b9b4..121b1a48c3a7 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -86,6 +86,30 @@ public function testItCanGenerateJsonApiResponse() ->assertJsonMissing(['jsonapi', 'included']); } + public function testItCanGenerateJsonApiResponseWithEmptyRelationship() + { + $user = UserFactory::new()->create(); + + $this->getJson('/users/'.$user->getKey().'?'.http_build_query(['includes' => ['posts']])) + ->assertHeader('Content-type', 'application/vnd.api+json') + ->assertExactJson([ + 'data' => [ + 'id' => (string) $user->getKey(), + 'type' => 'users', + 'attributes' => [ + 'name' => $user->name, + 'email' => $user->email, + ], + 'relationships' => [ + 'posts' => [ + 'data' => [], + ], + ], + ], + ]) + ->assertJsonMissing(['jsonapi', 'included']); + } + public function testItCanGenerateJsonApiResponseWithEagerLoadedRelationship() { $user = UserFactory::new()->create(); From a17ac8de8b9c509577561ea6f2cf436c066f30be Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 30 Oct 2025 13:07:23 -0500 Subject: [PATCH 63/74] remove asJsonApi from base resource --- .../Json/AnonymousResourceCollection.php | 17 ----------------- .../Http/Resources/Json/JsonResource.php | 13 ------------- 2 files changed, 30 deletions(-) diff --git a/src/Illuminate/Http/Resources/Json/AnonymousResourceCollection.php b/src/Illuminate/Http/Resources/Json/AnonymousResourceCollection.php index b3c10dfbe679..ba8c087f1194 100644 --- a/src/Illuminate/Http/Resources/Json/AnonymousResourceCollection.php +++ b/src/Illuminate/Http/Resources/Json/AnonymousResourceCollection.php @@ -2,8 +2,6 @@ namespace Illuminate\Http\Resources\Json; -use Illuminate\Http\Resources\JsonApi\AnonymousResourceCollection as JsonApiAnonymousResourceCollection; - class AnonymousResourceCollection extends ResourceCollection { /** @@ -32,19 +30,4 @@ public function __construct($resource, $collects) parent::__construct($resource); } - - /** - * Transform JSON resource to JSON:API. - * - * @param array $links - * @param array $meta - * @return Illuminate\Http\Resources\JsonApi\AnonymousResourceCollection - */ - public function asJsonApi(array $links = [], array $meta = []) - { - return new JsonApiAnonymousResourceCollection( - $this->collection->map(fn ($resource) => $resource->asJsonApi($links, $meta)), - $this->collects, - ); - } } diff --git a/src/Illuminate/Http/Resources/Json/JsonResource.php b/src/Illuminate/Http/Resources/Json/JsonResource.php index 209ced023591..f98f2cb53e45 100644 --- a/src/Illuminate/Http/Resources/Json/JsonResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonResource.php @@ -12,7 +12,6 @@ use Illuminate\Http\Request; use Illuminate\Http\Resources\ConditionallyLoadsAttributes; use Illuminate\Http\Resources\DelegatesToResource; -use Illuminate\Http\Resources\JsonApi\AnonymousJsonApiResource; use JsonException; use JsonSerializable; @@ -275,18 +274,6 @@ public function jsonSerialize(): array return $this->resolve(Container::getInstance()->make('request')); } - /** - * Transform JSON resource to JSON:API. - * - * @param array $links - * @param array $meta - * @return \Illuminate\Http\Resources\JsonApi\AnonymousJsonApiResource - */ - public function asJsonApi(array $links = [], array $meta = []) - { - return new AnonymousJsonApiResource($this, $links, $meta); - } - /** * Flush the resource's global state. * From 683601853185e585c94576cf7dd7c69ca028ccfd Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 30 Oct 2025 13:09:16 -0500 Subject: [PATCH 64/74] update exception --- .../Exceptions/ResourceIdentificationException.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Exceptions/ResourceIdentificationException.php b/src/Illuminate/Http/Resources/JsonApi/Exceptions/ResourceIdentificationException.php index 7fa44db31b5e..0a766dfa0a96 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Exceptions/ResourceIdentificationException.php +++ b/src/Illuminate/Http/Resources/JsonApi/Exceptions/ResourceIdentificationException.php @@ -7,22 +7,22 @@ class ResourceIdentificationException extends RuntimeException { /** - * Create a new unable to determine Resource ID exception for the given resource. + * Create an exception indicating we were unable to determine the resource ID for the given resource. * * @param mixed $resource - * @return static + * @return self */ public static function attemptingToDetermineIdFor($resource) { $resourceType = is_object($resource) ? $resource::class : gettype($resource); return new self(sprintf( - 'Unable to resolve resource object id for [%s].', $resourceType + 'Unable to resolve resource object ID for [%s].', $resourceType )); } /** - * Create a new unable to determine Resource type exception for the given resource. + * Create an exception indicating we were unable to determine the resource type for the given resource. * * @param mixed $resource * @return self From 5459428e0a3e2ba70a6db66fc8a3ab25b7d2bd01 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 30 Oct 2025 13:12:19 -0500 Subject: [PATCH 65/74] formatting --- .../JsonApi/AnonymousJsonApiResource.php | 39 ------------------- .../JsonApi/AnonymousResourceCollection.php | 2 +- 2 files changed, 1 insertion(+), 40 deletions(-) delete mode 100644 src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php diff --git a/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php deleted file mode 100644 index 18b5457c5a31..000000000000 --- a/src/Illuminate/Http/Resources/JsonApi/AnonymousJsonApiResource.php +++ /dev/null @@ -1,39 +0,0 @@ -resource); - - $this->jsonApiLinks = $links; - $this->jsonApiMeta = $meta; - } - - /** - * Transform the resource into an array. - * - * @param \Illuminate\Http\Request $request - * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable - */ - #[\Override] - public function toArray(Request $request) - { - return $this->jsonResource->toArray($request); - } -} diff --git a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php index e2f54ddb0880..08b35ed226aa 100644 --- a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php +++ b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php @@ -52,6 +52,6 @@ public function toArray(Request $request) #[\Override] public function withResponse(Request $request, JsonResponse $response): void { - $response->header('Content-type', 'application/vnd.api+json'); + $response->header('Content-Type', 'application/vnd.api+json'); } } From 4a5e36096b8e7fe797ad031df5f6f066233fd247 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 30 Oct 2025 13:14:10 -0500 Subject: [PATCH 66/74] fix test --- .../Http/Resources/JsonApi/JsonApiResourceTest.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index 121b1a48c3a7..ac95b9af7813 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -58,15 +58,6 @@ protected function afterRefreshingDatabase() }); } - public function testBaseJsonResourceCanBeConvertedToJsonApiResource() - { - $user = UserFactory::new()->create(); - - $resource = (new UserResource($user))->asJsonApi(); - - $this->assertInstanceOf(JsonApiResource::class, $resource); - } - public function testItCanGenerateJsonApiResponse() { $user = UserFactory::new()->create(); From 215b9bae897975b7ad3b086666c3dfb2e28623eb Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 30 Oct 2025 13:23:42 -0500 Subject: [PATCH 67/74] formatting --- .../ResolvesJsonApiSpecifications.php | 2 +- .../Resources/JsonApi/JsonApiResource.php | 92 ++++++++----------- 2 files changed, 37 insertions(+), 57 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php index 4aa094119a5d..351cea9e79c8 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php @@ -113,7 +113,7 @@ public function resolveResourceIncluded(Request $request): array $relations->push([ 'id' => $uniqueKey[1], 'type' => $uniqueKey[0], - 'attributes' => $resource->asJsonApi()->toArray($request), + 'attributes' => $resource->toArray($request), ]); } diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index cba19b2ce9f0..272c4e8356bd 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -57,32 +57,29 @@ public static function configure(?string $version = null, array $ext = [], array } /** - * Set the string that should wrap the outer-most resource array. + * Get the resource's ID. * - * @param string $value - * @return never - * - * @throws \RuntimeException + * @param \Illuminate\Http\Request $request + * @return string */ - #[\Override] - public static function wrap($value) + public function id(Request $request) { - throw new BadMethodCallException(sprintf('Using %s() method is not allowed.', __METHOD__)); + return $this->resolveResourceIdentifier($request); } /** - * Disable wrapping of the outer-most resource array. + * Get the resource's type. * - * @return never + * @param \Illuminate\Http\Request $request + * @return string */ - #[\Override] - public static function withoutWrapping() + public function type(Request $request) { - throw new BadMethodCallException(sprintf('Using %s() method is not allowed.', __METHOD__)); + return $this->resolveResourceType($request); } /** - * Resource "links" for JSON:API. + * Get the resource's links. * * @param \Illuminate\Http\Request $request * @return array @@ -93,7 +90,7 @@ public function links(Request $request) } /** - * Resource "meta" for JSON:API. + * Get the resource's meta information. * * @param \Illuminate\Http\Request $request * @return array @@ -103,28 +100,6 @@ public function meta(Request $request) return $this->jsonApiMeta; } - /** - * Resource "id" for JSON:API. - * - * @param \Illuminate\Http\Request $request - * @return string - */ - public function id(Request $request) - { - return $this->resolveResourceIdentifier($request); - } - - /** - * Resource "type" for JSON:API. - * - * @param \Illuminate\Http\Request $request - * @return string - */ - public function type(Request $request) - { - return $this->resolveResourceType($request); - } - /** * Get any additional data that should be returned with the resource array. * @@ -166,39 +141,44 @@ public function resolve($request = null) #[\Override] public function withResponse(Request $request, JsonResponse $response): void { - $response->header('Content-type', 'application/vnd.api+json'); + $response->header('Content-Type', 'application/vnd.api+json'); } /** - * Transform JSON resource to JSON:API. + * Create a new resource collection instance. * - * @param array $links - * @param array $meta - * @return $this + * @param mixed $resource + * @return \Illuminate\Http\Resources\JsonApi\AnonymousResourceCollection */ - public function asJsonApi(array $links = [], array $meta = []) + #[\Override] + protected static function newCollection($resource) { - if (! empty($links)) { - $this->jsonApiLinks = array_merge($this->jsonApiLinks, $links); - } - - if (! empty($meta)) { - $this->jsonApiMeta = array_merge($this->jsonApiMeta, $meta); - } + return new AnonymousResourceCollection($resource, static::class); + } - return $this; + /** + * Set the string that should wrap the outer-most resource array. + * + * @param string $value + * @return never + * + * @throws \RuntimeException + */ + #[\Override] + public static function wrap($value) + { + throw new BadMethodCallException(sprintf('Using %s() method is not allowed.', __METHOD__)); } /** - * Create a new resource collection instance. + * Disable wrapping of the outer-most resource array. * - * @param mixed $resource - * @return \Illuminate\Http\Resources\JsonApi\AnonymousResourceCollection + * @return never */ #[\Override] - protected static function newCollection($resource) + public static function withoutWrapping() { - return new AnonymousResourceCollection($resource, static::class); + throw new BadMethodCallException(sprintf('Using %s() method is not allowed.', __METHOD__)); } /** From 573a57aaed9f3ed22069cc0a3e5261053c326210 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 30 Oct 2025 13:48:59 -0500 Subject: [PATCH 68/74] formatting --- .../JsonApi/AnonymousResourceCollection.php | 2 +- .../ResolvesJsonApiSpecifications.php | 194 ++++++++---------- .../Resources/JsonApi/JsonApiResource.php | 2 +- 3 files changed, 88 insertions(+), 110 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php index 08b35ed226aa..4f66aad50947 100644 --- a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php +++ b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php @@ -18,7 +18,7 @@ public function with($request) { return array_filter([ 'included' => $this->collection - ->map(fn ($resource) => $resource->resolveResourceIncluded($request)) + ->map(fn ($resource) => $resource->resolveIncludedResources($request)) ->flatten(depth: 1) ->uniqueStrict(fn ($relation): array => [$relation['id'], $relation['type']]) ->all(), diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php index 351cea9e79c8..0d16cc8af8f1 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php @@ -20,21 +20,65 @@ trait ResolvesJsonApiSpecifications * * @var \WeakMap|null */ - protected $cachedLoadedRelationshipsMap; + protected $loadedRelationshipsMap; /** * Cached loaded relationships identifers. - * - * @var array */ - protected array $cachedLoadedRelationshipsIdentifier = []; + protected array $loadedRelationshipIdentifiers = []; + + /** + * Resolves `data` for the resource. + */ + public function resolveResourceData(Request $request): array + { + return [ + 'id' => $this->resolveResourceIdentifier($request), + 'type' => $this->resolveResourceType($request), + ...(new Collection([ + 'attributes' => $this->resolveResourceAttributes($request), + 'relationships' => $this->resolveResourceRelationshipIdentifiers($request), + 'links' => $this->resolveResourceLinks($request), + 'meta' => $this->resolveResourceMetaInformation($request), + ]))->filter()->map(fn ($value) => (object) $value), + ]; + } /** - * Resolves `attributes` for the resource's data object. + * Resolve the resource's identifier. * - * @param \Illuminate\Http\Request $request * @return string|int * + * @throws ResourceIdentificationException + */ + protected function resolveResourceIdentifier(Request $request): string + { + if (! $this->resource instanceof Model) { + throw ResourceIdentificationException::attemptingToDetermineIdFor($this); + } + + return static::resourceIdFromModel($this->resource); + } + + /** + * Resolve the resource's type. + * + * + * @throws ResourceIdentificationException + */ + protected function resolveResourceType(Request $request): string + { + if (! $this->resource instanceof Model) { + throw ResourceIdentificationException::attemptingToDetermineTypeFor($this); + } + + return static::resourceTypeFromModel($this->resource); + } + + /** + * Resolve the resource's attributes. + * + * * @throws \RuntimeException */ protected function resolveResourceAttributes(Request $request): array @@ -57,12 +101,11 @@ protected function resolveResourceAttributes(Request $request): array /** * Resolves `relationships` for the resource's data object. * - * @param \Illuminate\Http\Request $request * @return string|int * * @throws \RuntimeException */ - protected function resolveResourceRelationshipsIdentifiers(Request $request): array + protected function resolveResourceRelationshipIdentifiers(Request $request): array { if (! $this->resource instanceof Model) { return []; @@ -71,81 +114,33 @@ protected function resolveResourceRelationshipsIdentifiers(Request $request): ar $this->compileResourceRelationships($request); return [ - ...$this->cachedLoadedRelationshipsIdentifier, + ...$this->loadedRelationshipIdentifiers, ]; } - /** - * Resolves `data` for the resource. - * - * @param \Illuminate\Http\Request $request - * @return array - */ - public function resolveResourceData(Request $request): array - { - return [ - 'id' => $this->resolveResourceIdentifier($request), - 'type' => $this->resolveResourceType($request), - ...(new Collection([ - 'attributes' => $this->resolveResourceAttributes($request), - 'relationships' => $this->resolveResourceRelationshipsIdentifiers($request), - 'links' => $this->resolveResourceLinks($request), - 'meta' => $this->resolveMetaInformations($request), - ]))->filter()->map(fn ($value) => (object) $value), - ]; - } - - /** - * Resolves `included` for the resource. - * - * @param \Illuminate\Http\Request $request - * @return array - */ - public function resolveResourceIncluded(Request $request): array - { - $this->compileResourceRelationships($request); - - $relations = new Collection(); - - foreach ($this->cachedLoadedRelationshipsMap as $relation => $uniqueKey) { - $resource = rescue(fn () => $relation->toResource(), new JsonApiResource($relation), false); - - $relations->push([ - 'id' => $uniqueKey[1], - 'type' => $uniqueKey[0], - 'attributes' => $resource->toArray($request), - ]); - } - - return $relations->uniqueStrict(fn ($relation): array => [$relation['id'], $relation['type']])->all(); - } - /** * Compile resource relationships. - * - * @param \Illuminate\Http\Request $request - * @return void */ protected function compileResourceRelationships(Request $request): void { - if ($this->cachedLoadedRelationshipsMap instanceof WeakMap) { + if ($this->loadedRelationshipsMap instanceof WeakMap) { return; } - $this->cachedLoadedRelationshipsMap = new WeakMap; + $this->loadedRelationshipsMap = new WeakMap; - $this->cachedLoadedRelationshipsIdentifier = (new Collection($this->resource->getRelations())) + $this->loadedRelationshipIdentifiers = (new Collection($this->resource->getRelations())) ->mapWithKeys(function ($relations, $key) { if ($relations instanceof Collection) { if ($relations->isEmpty()) { return [$key => ['data' => $relations]]; } - $key = static::getResourceTypeFromEloquent($relations->first()); + $key = static::resourceTypeFromModel($relations->first()); return [$key => ['data' => $relations->map(function ($relation) use ($key) { - return transform([$key, static::getResourceIdFromEloquent($relation)], function ($uniqueKey) use ($relation) { - $this->cachedLoadedRelationshipsMap[$relation] = $uniqueKey; + return transform([$key, static::resourceIdFromModel($relation)], function ($uniqueKey) use ($relation) { + $this->loadedRelationshipsMap[$relation] = $uniqueKey; return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]]; }); @@ -153,9 +148,9 @@ protected function compileResourceRelationships(Request $request): void } return [$key => ['data' => transform( - [static::getResourceTypeFromEloquent($relations), static::getResourceIdFromEloquent($relations)], + [static::resourceTypeFromModel($relations), static::resourceIdFromModel($relations)], function ($uniqueKey) use ($relations) { - $this->cachedLoadedRelationshipsMap[$relations] = $uniqueKey; + $this->loadedRelationshipsMap[$relations] = $uniqueKey; return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]]; } @@ -164,43 +159,32 @@ function ($uniqueKey) use ($relations) { } /** - * Resolves `id` for the resource. - * - * @param \Illuminate\Http\Request $request - * @return string|int - * - * @throws \RuntimeException + * Resolves `included` for the resource. */ - protected function resolveResourceIdentifier(Request $request): string + public function resolveIncludedResources(Request $request): array { - if ($this->resource instanceof Model) { - return static::getResourceIdFromEloquent($this->resource); - } + $this->compileResourceRelationships($request); - throw ResourceIdentificationException::attemptingToDetermineIdFor($this); - } + $relations = new Collection; - /** - * Resolves `type` for the resource. - * - * @param \Illuminate\Http\Request $request - * @return string - * - * @throws \RuntimeException - */ - protected function resolveResourceType(Request $request): string - { - if ($this->resource instanceof Model) { - return static::getResourceTypeFromEloquent($this->resource); + foreach ($this->loadedRelationshipsMap as $relation => $uniqueKey) { + $resource = rescue(fn () => $relation->toResource(), new JsonApiResource($relation), false); + + $relations->push([ + 'id' => $uniqueKey[1], + 'type' => $uniqueKey[0], + 'attributes' => $resource->toArray($request), + ]); } - throw ResourceIdentificationException::attemptingToDetermineTypeFor($this); + return $relations->uniqueStrict( + fn ($relation): array => [$relation['id'], $relation['type']] + )->all(); } /** - * Resolves `links` object for the resource. + * Resolve the links for the resource. * - * @param \Illuminate\Http\Request $request * @return array */ protected function resolveResourceLinks(Request $request): array @@ -209,40 +193,34 @@ protected function resolveResourceLinks(Request $request): array } /** - * Resolves `meta` object for the resource. + * Resolve the meta information for the resource. * - * @param \Illuminate\Http\Request $request * @return array */ - protected function resolveMetaInformations(Request $request): array + protected function resolveResourceMetaInformation(Request $request): array { return $this->meta($request); } /** - * Get expected resource ID from eloquent model. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @return string + * Get the resource ID from the given Eloquent model. */ - protected static function getResourceIdFromEloquent(Model $model): string + protected static function resourceIdFromModel(Model $model): string { return $model->getKey(); } /** - * Get expected resource type from eloquent model. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @return string + * Get the resource type from the given Eloquent model. */ - protected static function getResourceTypeFromEloquent(Model $model): string + protected static function resourceTypeFromModel(Model $model): string { $modelClassName = $model::class; - $morphMap = Relation::getMorphAlias($modelClassName); - $modelBaseName = $morphMap !== $modelClassName ? $morphMap : class_basename($modelClassName); + $morphMap = Relation::getMorphAlias($modelClassName); - return Str::of($modelBaseName)->snake()->pluralStudly(); + return Str::of( + $morphMap !== $modelClassName ? $morphMap : class_basename($modelClassName) + )->snake()->pluralStudly(); } } diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index 272c4e8356bd..f07192bd5df0 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -110,7 +110,7 @@ public function meta(Request $request) public function with($request) { return array_filter([ - 'included' => $this->resolveResourceIncluded($request), + 'included' => $this->resolveIncludedResources($request), ...($implementation = static::$jsonApiInformation) ? ['jsonapi' => $implementation] : [], From 115ed94164782aef60db048b053eb532a1cb448b Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 30 Oct 2025 13:53:13 -0500 Subject: [PATCH 69/74] formatting --- .../Http/Resources/JsonApi/JsonApiResource.php | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index f07192bd5df0..310af0cdd573 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -28,22 +28,17 @@ class JsonApiResource extends JsonResource /** * The resource's "links" for JSON:API. - * - * @var array */ protected array $jsonApiLinks = []; /** * The resource's "meta" for JSON:API. - * - * @var array */ protected array $jsonApiMeta = []; /** * Set the JSON:API version for the request. * - * @param string $version * @return void */ public static function configure(?string $version = null, array $ext = [], array $profile = [], array $meta = []) @@ -59,7 +54,6 @@ public static function configure(?string $version = null, array $ext = [], array /** * Get the resource's ID. * - * @param \Illuminate\Http\Request $request * @return string */ public function id(Request $request) @@ -70,7 +64,6 @@ public function id(Request $request) /** * Get the resource's type. * - * @param \Illuminate\Http\Request $request * @return string */ public function type(Request $request) @@ -81,7 +74,6 @@ public function type(Request $request) /** * Get the resource's links. * - * @param \Illuminate\Http\Request $request * @return array */ public function links(Request $request) @@ -92,7 +84,6 @@ public function links(Request $request) /** * Get the resource's meta information. * - * @param \Illuminate\Http\Request $request * @return array */ public function meta(Request $request) @@ -133,10 +124,6 @@ public function resolve($request = null) /** * Customize the outgoing response for the resource. - * - * @param \Illuminate\Http\Request $request - * @param \Illuminate\Http\JsonResponse $response - * @return void */ #[\Override] public function withResponse(Request $request, JsonResponse $response): void From 1da745b05fd79ed2523f327692702f6f7eee0ed3 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 30 Oct 2025 13:54:43 -0500 Subject: [PATCH 70/74] rename trait --- ...vesJsonApiSpecifications.php => ResolvesJsonApiElements.php} | 2 +- src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/Illuminate/Http/Resources/JsonApi/Concerns/{ResolvesJsonApiSpecifications.php => ResolvesJsonApiElements.php} (99%) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php similarity index 99% rename from src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php rename to src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 0d16cc8af8f1..909fb7c4416c 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiSpecifications.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -13,7 +13,7 @@ use JsonSerializable; use WeakMap; -trait ResolvesJsonApiSpecifications +trait ResolvesJsonApiElements { /** * Cached loaded relationships map. diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index 310af0cdd573..0c592876a3fb 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -10,7 +10,7 @@ class JsonApiResource extends JsonResource { - use Concerns\ResolvesJsonApiSpecifications; + use Concerns\ResolvesJsonApiElements; /** * The "data" wrapper that should be applied. From 4abf43a6579c360acb9fb6e634e8993e85b4ec9f Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Sat, 1 Nov 2025 01:08:51 +0800 Subject: [PATCH 71/74] [JSON:API] Add `toAttributes()` method (#57603) * [JSON:API] Add `toAttributes()` method Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki * formatting --------- Signed-off-by: Mior Muhammad Zaki Co-authored-by: StyleCI Bot Co-authored-by: Taylor Otwell --- .../Http/Resources/Json/JsonResource.php | 13 +++++++++- .../Resources/Json/ResourceCollection.php | 4 +-- .../JsonApi/AnonymousResourceCollection.php | 2 +- .../Concerns/ResolvesJsonApiElements.php | 26 +++++++++++++++---- .../Resources/JsonApi/JsonApiResource.php | 16 ++++++------ .../Resources/JsonApi/JsonApiResourceTest.php | 8 +++--- 6 files changed, 48 insertions(+), 21 deletions(-) diff --git a/src/Illuminate/Http/Resources/Json/JsonResource.php b/src/Illuminate/Http/Resources/Json/JsonResource.php index f98f2cb53e45..f55fc71461e7 100644 --- a/src/Illuminate/Http/Resources/Json/JsonResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonResource.php @@ -111,7 +111,7 @@ protected static function newCollection($resource) */ public function resolve($request = null) { - $data = $this->toArray( + $data = $this->toAttributes( $request ?: Container::getInstance()->make('request') ); @@ -124,6 +124,17 @@ public function resolve($request = null) return $this->filter((array) $data); } + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toAttributes(Request $request) + { + return $this->toArray($request); + } + /** * Transform the resource into an array. * diff --git a/src/Illuminate/Http/Resources/Json/ResourceCollection.php b/src/Illuminate/Http/Resources/Json/ResourceCollection.php index 81cfc1bd3181..025ab9b95f42 100644 --- a/src/Illuminate/Http/Resources/Json/ResourceCollection.php +++ b/src/Illuminate/Http/Resources/Json/ResourceCollection.php @@ -96,9 +96,9 @@ public function count(): int * @param \Illuminate\Http\Request $request * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable */ - public function toArray(Request $request) + public function toAttributes(Request $request) { - return $this->collection->map->toArray($request)->all(); + return $this->collection->map->resolve($request)->all(); } /** diff --git a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php index 4f66aad50947..ebc314652249 100644 --- a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php +++ b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php @@ -35,7 +35,7 @@ public function with($request) * @return array */ #[\Override] - public function toArray(Request $request) + public function toAttributes(Request $request) { return $this->collection ->map(fn ($resource) => $resource->resolveResourceData($request)) diff --git a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 909fb7c4416c..67469fd9c656 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -6,8 +6,10 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Http\Request; +use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\JsonApi\Exceptions\ResourceIdentificationException; use Illuminate\Http\Resources\JsonApi\JsonApiResource; +use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Str; use JsonSerializable; @@ -53,6 +55,10 @@ public function resolveResourceData(Request $request): array */ protected function resolveResourceIdentifier(Request $request): string { + if (! is_null($resourceId = $this->toId($request))) { + return $resourceId; + } + if (! $this->resource instanceof Model) { throw ResourceIdentificationException::attemptingToDetermineIdFor($this); } @@ -68,6 +74,10 @@ protected function resolveResourceIdentifier(Request $request): string */ protected function resolveResourceType(Request $request): string { + if (! is_null($resourceType = $this->toType($request))) { + return $resourceType; + } + if (! $this->resource instanceof Model) { throw ResourceIdentificationException::attemptingToDetermineTypeFor($this); } @@ -83,7 +93,7 @@ protected function resolveResourceType(Request $request): string */ protected function resolveResourceAttributes(Request $request): array { - $data = $this->toArray($request); + $data = $this->toAttributes($request); if ($data instanceof Arrayable) { $data = $data->toArray(); @@ -92,6 +102,7 @@ protected function resolveResourceAttributes(Request $request): array } $data = (new Collection($data)) + ->mapWithKeys(fn ($value, $key) => is_int($key) ? [$value => $this->resource->{$value}] : [$key => $value]) ->transform(fn ($value) => value($value, $request)) ->all(); @@ -168,12 +179,17 @@ public function resolveIncludedResources(Request $request): array $relations = new Collection; foreach ($this->loadedRelationshipsMap as $relation => $uniqueKey) { - $resource = rescue(fn () => $relation->toResource(), new JsonApiResource($relation), false); + $resourceInstance = rescue(fn () => $relation->toResource(), new JsonApiResource($relation), false); + + if (! $resourceInstance instanceof JsonApiResource && + $resourceInstance instanceof JsonResource) { + $resourceInstance = new JsonApiResource($resourceInstance->resource); + } $relations->push([ 'id' => $uniqueKey[1], 'type' => $uniqueKey[0], - 'attributes' => $resource->toArray($request), + 'attributes' => Arr::get($resourceInstance->resolve($request), 'data.attributes', []), ]); } @@ -189,7 +205,7 @@ public function resolveIncludedResources(Request $request): array */ protected function resolveResourceLinks(Request $request): array { - return $this->links($request); + return $this->toLinks($request); } /** @@ -199,7 +215,7 @@ protected function resolveResourceLinks(Request $request): array */ protected function resolveResourceMetaInformation(Request $request): array { - return $this->meta($request); + return $this->toMeta($request); } /** diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index 0c592876a3fb..b595332790d0 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -54,21 +54,21 @@ public static function configure(?string $version = null, array $ext = [], array /** * Get the resource's ID. * - * @return string + * @return string|null */ - public function id(Request $request) + public function toId(Request $request) { - return $this->resolveResourceIdentifier($request); + return null; } /** * Get the resource's type. * - * @return string + * @return string|null */ - public function type(Request $request) + public function toType(Request $request) { - return $this->resolveResourceType($request); + return null; } /** @@ -76,7 +76,7 @@ public function type(Request $request) * * @return array */ - public function links(Request $request) + public function toLinks(Request $request) { return $this->jsonApiLinks; } @@ -86,7 +86,7 @@ public function links(Request $request) * * @return array */ - public function meta(Request $request) + public function toMeta(Request $request) { return $this->jsonApiMeta; } diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index ac95b9af7813..c86b3a5bd261 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -166,7 +166,7 @@ public function toArray(Request $request) class UserApiResource extends JsonApiResource { - public function toArray(Request $request) + public function toAttributes(Request $request) { return [ 'name' => $this->name, @@ -186,11 +186,11 @@ public function user() class PostApiResource extends JsonApiResource { - public function toArray(Request $request) + public function toAttributes(Request $request) { return [ - 'title' => $this->title, - 'content' => $this->content, + 'title', + 'content', ]; } } From 44515944fd970697fd62c971e87b61af5910fbf9 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Tue, 4 Nov 2025 06:09:07 +0800 Subject: [PATCH 72/74] [JSON:API] Allows `JsonApiResource::$attributes` (#57638) * [JSON:API] Allows `JsonApiResource::$attributes` Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * Update tests Signed-off-by: Mior Muhammad Zaki * formatting --------- Signed-off-by: Mior Muhammad Zaki Co-authored-by: StyleCI Bot Co-authored-by: Taylor Otwell --- .../Http/Resources/JsonApi/JsonApiResource.php | 16 ++++++++++++++++ .../Resources/JsonApi/JsonApiResourceTest.php | 11 ++++------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index b595332790d0..975b45d49638 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -71,6 +71,22 @@ public function toType(Request $request) return null; } + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Contracts\Support\Arrayable|\JsonSerializable|array + */ + #[\Override] + public function toAttributes(Request $request) + { + if (property_exists($this, 'attributes')) { + return $this->attributes; + } + + return $this->toArray($request); + } + /** * Get the resource's links. * diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index c86b3a5bd261..06dafe207840 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -186,13 +186,10 @@ public function user() class PostApiResource extends JsonApiResource { - public function toAttributes(Request $request) - { - return [ - 'title', - 'content', - ]; - } + protected array $attributes = [ + 'title', + 'content', + ]; } class PostFactory extends Factory From be536df04b889f90ddcc79c7d8950fce51005991 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Sat, 8 Nov 2025 05:16:02 +0800 Subject: [PATCH 73/74] [JSON:API] Supports `JsonApiResource::toRelationships()` (#57646) * [JSON:API] Supports `JsonApiResource::toRelationships()` Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * Update JsonApiResource.php * only include relationship if defined * formatting * tweak how relations work * work on relationships * fixes implementation Signed-off-by: Mior Muhammad Zaki * Add `JsonApiRequest` Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * formatting * wip Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * Add `make:resource` with `--json-api` option Signed-off-by: Mior Muhammad Zaki * Apply suggestions from code review * ensure request is always `JsonApiRequest` * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI --------- Signed-off-by: Mior Muhammad Zaki Co-authored-by: StyleCI Bot Co-authored-by: Taylor Otwell --- .../Console/ResourceMakeCommand.php | 9 +- .../Console/stubs/resource-json-api.stub | 19 ++ .../Http/Resources/Json/JsonResource.php | 16 +- .../JsonApi/AnonymousResourceCollection.php | 27 +- .../Concerns/ResolvesJsonApiElements.php | 127 +++++++--- .../Concerns/ResolvesJsonApiRequest.php | 21 ++ .../Http/Resources/JsonApi/JsonApiRequest.php | 33 +++ .../Resources/JsonApi/JsonApiResource.php | 44 +++- .../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 +++ .../JsonApi/JsonApiCollectionTest.php | 209 ++++++++++++++++ .../Resources/JsonApi/JsonApiRequestTest.php | 47 ++++ .../Resources/JsonApi/JsonApiResourceTest.php | 235 ++++++++---------- .../Http/Resources/JsonApi/TestCase.php | 46 ++++ 24 files changed, 930 insertions(+), 178 deletions(-) create mode 100644 src/Illuminate/Foundation/Console/stubs/resource-json-api.stub create mode 100644 src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiRequest.php create mode 100644 src/Illuminate/Http/Resources/JsonApi/JsonApiRequest.php 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/JsonApiCollectionTest.php create mode 100644 tests/Integration/Http/Resources/JsonApi/JsonApiRequestTest.php create mode 100644 tests/Integration/Http/Resources/JsonApi/TestCase.php 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..ba554d366b58 --- /dev/null +++ b/src/Illuminate/Foundation/Console/stubs/resource-json-api.stub @@ -0,0 +1,19 @@ +|array + */ + public function toAttributes(Request $request): array + { + return parent::toAttributes($request); + } +} diff --git a/src/Illuminate/Http/Resources/Json/JsonResource.php b/src/Illuminate/Http/Resources/Json/JsonResource.php index f55fc71461e7..02ebce156d8c 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) { @@ -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. * @@ -260,7 +270,7 @@ public static function withoutWrapping() public function response($request = null) { return $this->toResponse( - $request ?: Container::getInstance()->make('request') + $request ?: $this->resolveRequestFromContainer() ); } @@ -282,7 +292,7 @@ public function toResponse($request) */ public function jsonSerialize(): array { - return $this->resolve(Container::getInstance()->make('request')); + return $this->resolve($this->resolveRequestFromContainer()); } /** diff --git a/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php b/src/Illuminate/Http/Resources/JsonApi/AnonymousResourceCollection.php index ebc314652249..08247507ca36 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. * @@ -20,7 +23,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] @@ -54,4 +56,27 @@ 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. + * + * @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 67469fd9c656..2b2a4ef4e5da 100644 --- a/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -4,11 +4,15 @@ 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; 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\MissingValue; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Str; @@ -32,13 +36,15 @@ trait ResolvesJsonApiElements /** * Resolves `data` for the resource. */ - public function resolveResourceData(Request $request): array + 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), @@ -53,7 +59,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; @@ -72,7 +78,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; @@ -91,7 +97,7 @@ protected function resolveResourceType(Request $request): string * * @throws \RuntimeException */ - protected function resolveResourceAttributes(Request $request): array + protected function resolveResourceAttributes(JsonApiRequest $request, string $resourceType): array { $data = $this->toAttributes($request); @@ -101,8 +107,11 @@ protected function resolveResourceAttributes(Request $request): array $data = $data->jsonSerialize(); } + $sparseFieldset = $request->sparseFields($resourceType); + $data = (new Collection($data)) ->mapWithKeys(fn ($value, $key) => is_int($key) ? [$value => $this->resource->{$value}] : [$key => $value]) + ->when(! empty($sparseFieldset), fn ($attributes) => $attributes->only($sparseFieldset)) ->transform(fn ($value) => value($value, $request)) ->all(); @@ -116,7 +125,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 []; @@ -125,60 +134,95 @@ protected function resolveResourceRelationshipIdentifiers(Request $request): arr $this->compileResourceRelationships($request); return [ - ...$this->loadedRelationshipIdentifiers, + ...(new Collection($this->filter($this->loadedRelationshipIdentifiers))) + ->map(function ($relation) { + return ! is_null($relation) ? $relation : ['data' => []]; + })->all(), ]; } /** * Compile resource relationships. */ - protected function compileResourceRelationships(Request $request): void + protected function compileResourceRelationships(JsonApiRequest $request): void { if ($this->loadedRelationshipsMap instanceof WeakMap) { return; } - $this->loadedRelationshipsMap = new WeakMap; + $sparseIncluded = $request->sparseIncluded(); + + $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)); + + $resourceRelationshipKeys = $resourceRelationships->keys(); + + $this->resource->loadMissing($resourceRelationshipKeys->all()); - $this->loadedRelationshipIdentifiers = (new Collection($this->resource->getRelations())) - ->mapWithKeys(function ($relations, $key) { - if ($relations instanceof Collection) { - if ($relations->isEmpty()) { - return [$key => ['data' => $relations]]; - } + $this->loadedRelationshipsMap = new WeakMap; - $key = static::resourceTypeFromModel($relations->first()); + $this->loadedRelationshipIdentifiers = $resourceRelationships->mapWithKeys(function ($relationResolver, $key) { + $relatedModels = value($relationResolver); - return [$key => ['data' => $relations->map(function ($relation) use ($key) { - return transform([$key, static::resourceIdFromModel($relation)], function ($uniqueKey) use ($relation) { - $this->loadedRelationshipsMap[$relation] = $uniqueKey; + // Relationship is a collection of models... + if ($relatedModels instanceof Collection) { + $relatedModels = $relatedModels->values(); - return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]]; - }); - })]]; + if ($relatedModels->isEmpty()) { + return [$key => ['data' => $relatedModels]]; } - return [$key => ['data' => transform( - [static::resourceTypeFromModel($relations), static::resourceIdFromModel($relations)], - function ($uniqueKey) use ($relations) { - $this->loadedRelationshipsMap[$relations] = $uniqueKey; + $relationship = $this->resource->{$key}(); + + $isUnique = ! $relationship instanceof BelongsToMany; + + $key = static::resourceTypeFromModel($relatedModels->first()); + + return [$key => ['data' => $relatedModels->map(function ($relation) use ($key, $isUnique) { + return transform([$key, static::resourceIdFromModel($relation)], function ($uniqueKey) use ($relation, $isUnique) { + $this->loadedRelationshipsMap[$relation] = [...$uniqueKey, $isUnique]; return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]]; - } - )]]; - })->all(); + }); + })]]; + } + + // Relationship is a single model... + $relatedModel = $relatedModels; + + if (is_null($relatedModel)) { + return [$key => null]; + } elseif ($relatedModel instanceof Pivot || + in_array(AsPivot::class, class_uses_recursive($relatedModel), true)) { + return [$key => new MissingValue]; + } + + return [$key => ['data' => [transform( + [static::resourceTypeFromModel($relatedModel), static::resourceIdFromModel($relatedModel)], + function ($uniqueKey) use ($relatedModel) { + $this->loadedRelationshipsMap[$relatedModel] = [...$uniqueKey, true]; + + return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]]; + } + )]]]; + })->all(); } /** * Resolves `included` for the resource. */ - public function resolveIncludedResources(Request $request): array + public function resolveIncludedResources(JsonApiRequest $request): array { + if (! $this->resource instanceof Model) { + return []; + } + $this->compileResourceRelationships($request); $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 && @@ -186,16 +230,19 @@ public function resolveIncludedResources(Request $request): array $resourceInstance = new JsonApiResource($resourceInstance->resource); } + [$type, $id, $isUnique] = $value; + $relations->push([ - 'id' => $uniqueKey[1], - 'type' => $uniqueKey[0], + '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']] - )->all(); + return $relations->uniqueStrict(fn ($relation) => $relation['_uniqueKey']) + ->map(fn ($relation) => Arr::except($relation, ['_uniqueKey'])) + ->all(); } /** @@ -203,7 +250,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); } @@ -213,7 +260,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..c24dd927d925 --- /dev/null +++ b/src/Illuminate/Http/Resources/JsonApi/Concerns/ResolvesJsonApiRequest.php @@ -0,0 +1,21 @@ +array('fields'), $key, ''); + + return empty($fieldsets) + ? [] + : explode(',', $fieldsets); + } + + /** + * Get the request's included relationships. + */ + public function sparseIncluded(): array + { + $included = (string) $this->string('include', ''); + + return empty($included) + ? [] + : explode(',', $included); + } +} diff --git a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php index 975b45d49638..003ffa41a6d1 100644 --- a/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php +++ b/src/Illuminate/Http/Resources/JsonApi/JsonApiResource.php @@ -6,11 +6,11 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; -use Illuminate\Support\Collection; class JsonApiResource extends JsonResource { - use Concerns\ResolvesJsonApiElements; + use Concerns\ResolvesJsonApiElements, + Concerns\ResolvesJsonApiRequest; /** * The "data" wrapper that should be applied. @@ -87,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. * @@ -134,7 +149,7 @@ public function with($request) public function resolve($request = null) { return [ - 'data' => $this->resolveResourceData($request), + 'data' => $this->resolveResourceData($this->resolveJsonApiRequestFrom($request ?? $this->resolveRequestFromContainer())), ]; } @@ -147,6 +162,29 @@ 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. + * + * @return \Illuminate\Http\Resources\JsonApi\JsonApiRequest + */ + #[\Override] + protected function resolveRequestFromContainer() + { + return $this->resolveJsonApiRequestFrom(parent::resolveRequestFromContainer()); + } + /** * Create a new resource collection instance. * 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/JsonApiCollectionTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiCollectionTest.php new file mode 100644 index 000000000000..b295eddd36cb --- /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/JsonApiRequestTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiRequestTest.php new file mode 100644 index 000000000000..39af5098153a --- /dev/null +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiRequestTest.php @@ -0,0 +1,47 @@ + [ + '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()); + } +} diff --git a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php index 06dafe207840..fe166c5c10ba 100644 --- a/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php +++ b/tests/Integration/Http/Resources/JsonApi/JsonApiResourceTest.php @@ -2,67 +2,37 @@ 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\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\WithMigration; -use Orchestra\Testbench\Factories\UserFactory; -use Orchestra\Testbench\TestCase; +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; -#[WithMigration] class JsonApiResourceTest extends TestCase { - use RefreshDatabase; - - /** {@inheritdoc} */ - #[\Override] - protected function tearDown(): void - { - parent::tearDown(); - - JsonResource::flushState(); - JsonApiResource::flushState(); - } - - /** {@inheritdoc} */ - #[\Override] - protected function defineRoutes($router) + public function testItCanGenerateJsonApiResponse() { - $router->get('users/{userId}', function (Request $request, $userId) { - $user = User::find($userId); + $user = User::factory()->create(); - if (! empty($includes = $request->array('includes'))) { - $user->loadMissing($includes); - } - - return $user->toResource(); - }); - } - - /** {@inheritdoc} */ - protected function afterRefreshingDatabase() - { - Schema::create('posts', function (Blueprint $table) { - $table->id(); - $table->foreignId('user_id'); - $table->string('title'); - $table->text('content'); - $table->timestamps(); - }); + $this->getJson("/users/{$user->getKey()}") + ->assertHeader('Content-type', 'application/vnd.api+json') + ->assertExactJson([ + 'data' => [ + 'id' => (string) $user->getKey(), + 'type' => 'users', + 'attributes' => [ + 'name' => $user->name, + 'email' => $user->email, + ], + ], + ]) + ->assertJsonMissing(['jsonapi', 'included']); } - public function testItCanGenerateJsonApiResponse() + public function testItCanGenerateJsonApiResponseWithSparseFieldsets() { - $user = UserFactory::new()->create(); + $user = User::factory()->create(); - $this->getJson('/users/'.$user->getKey()) + $this->getJson("/users/{$user->getKey()}?".http_build_query(['fields' => ['users' => 'name']])) ->assertHeader('Content-type', 'application/vnd.api+json') ->assertExactJson([ 'data' => [ @@ -70,18 +40,17 @@ public function testItCanGenerateJsonApiResponse() 'type' => 'users', 'attributes' => [ 'name' => $user->name, - 'email' => $user->email, ], ], ]) ->assertJsonMissing(['jsonapi', 'included']); } - public function testItCanGenerateJsonApiResponseWithEmptyRelationship() + public function testItCanGenerateJsonApiResponseWithEmptyRelationshipsUsingSparseIncluded() { - $user = UserFactory::new()->create(); + $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' => [ @@ -101,15 +70,30 @@ public function testItCanGenerateJsonApiResponseWithEmptyRelationship() ->assertJsonMissing(['jsonapi', 'included']); } - public function testItCanGenerateJsonApiResponseWithEagerLoadedRelationship() + public function testItCanGenerateJsonApiResponseWithRelationshipsUsingSparseIncluded() { - $user = UserFactory::new()->create(); + $now = $this->freezeSecond(); + + $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 = PostFactory::new()->times(2)->create([ + $posts = Post::factory()->times(2)->create([ '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' => [ @@ -126,86 +110,81 @@ 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'], + ['id' => (string) $team->getKey(), 'type' => 'teams'], + ], + ], ], ], 'included' => [ + [ + 'id' => (string) $profile->getKey(), + 'type' => 'profiles', + 'attributes' => [ + 'date_of_birth' => '2011-06-09', + 'id' => $profile->getKey(), + 'timezone' => 'America/Chicago', + 'user_id' => $user->getKey(), + ], + ], [ '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) $team->getKey(), + 'type' => 'teams', + 'attributes' => [ + 'id' => $team->getKey(), + 'user_id' => $team->user_id, + 'name' => 'Laravel Team', + 'personal_team' => true, + 'membership' => [ + 'created_at' => $now->toISOString(), + 'role' => 'Admin', + 'team_id' => $team->getKey(), + 'user_id' => $user->getKey(), + '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' => [ + 'created_at' => $now->toISOString(), + 'role' => 'Member', + 'team_id' => $team->getKey(), + 'user_id' => $user->getKey(), + 'updated_at' => $now->toISOString(), + ], + ], ], ], ]); } } - -#[UseResource(UserApiResource::class)] -class User extends Authenticatable -{ - public function posts() - { - return $this->hasMany(Post::class); - } -} - -class UserResource extends JsonResource -{ - public function toArray(Request $request) - { - return [ - 'name' => $this->name, - 'email' => $this->email, - ]; - } -} - -class UserApiResource extends JsonApiResource -{ - 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; - } -} diff --git a/tests/Integration/Http/Resources/JsonApi/TestCase.php b/tests/Integration/Http/Resources/JsonApi/TestCase.php new file mode 100644 index 000000000000..a1644383a7cb --- /dev/null +++ b/tests/Integration/Http/Resources/JsonApi/TestCase.php @@ -0,0 +1,46 @@ +get('users', function () { + return User::paginate(5)->toResourceCollection(); + }); + + $router->get('users/{userId}', function ($userId) { + return User::find($userId)->toResource(); + }); + } + + /** {@inheritdoc} */ + protected function afterRefreshingDatabase() + { + require __DIR__.'/Fixtures/migrations.php'; + } +} From ea1ea07cedece2863b4b5f50bcdb6ccca3bd2170 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 7 Nov 2025 15:25:15 -0600 Subject: [PATCH 74/74] formatting --- .../Foundation/Console/ResourceMakeCommand.php | 2 +- .../Console/stubs/resource-json-api.stub | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Illuminate/Foundation/Console/ResourceMakeCommand.php b/src/Illuminate/Foundation/Console/ResourceMakeCommand.php index 2b6d8927204e..9b57b818c9a1 100644 --- a/src/Illuminate/Foundation/Console/ResourceMakeCommand.php +++ b/src/Illuminate/Foundation/Console/ResourceMakeCommand.php @@ -102,8 +102,8 @@ 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'], + ['collection', 'c', InputOption::VALUE_NONE, 'Create a resource collection'], ]; } } diff --git a/src/Illuminate/Foundation/Console/stubs/resource-json-api.stub b/src/Illuminate/Foundation/Console/stubs/resource-json-api.stub index ba554d366b58..fe1137702506 100644 --- a/src/Illuminate/Foundation/Console/stubs/resource-json-api.stub +++ b/src/Illuminate/Foundation/Console/stubs/resource-json-api.stub @@ -8,12 +8,16 @@ use Illuminate\Http\Resources\JsonApi\JsonApiResource; class {{ class }} extends JsonApiResource { /** - * Transform the resource into an array. - * - * @return list|array + * The resource's attributes. */ - public function toAttributes(Request $request): array - { - return parent::toAttributes($request); - } + public $attributes = [ + // ... + ]; + + /** + * The resource's relationships. + */ + public $relationships = [ + // ... + ]; }