From ed7a98395bc570046c5fdf4a1d200b3b8e1db1ad Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Sun, 12 Dec 2021 10:52:21 +1100 Subject: [PATCH 01/15] credit @jessarcher !!! --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6eefa80..3015745 100644 --- a/README.md +++ b/README.md @@ -330,6 +330,7 @@ Relationships can be resolved deeply and also multiple relationship paths can be ## Credits - [Tim MacDonald](https://github.com/timacdonald) +- [Jess Archer](https://github.com/jessarcher) for co-creating our initial in-house version - [All Contributors](../../contributors) And a special (vegi) thanks to [Caneco](https://twitter.com/caneco) for the logo ✨ From 64c9786b5e5ba9b276e124eef58e63212e7376b7 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Sun, 12 Dec 2021 10:53:46 +1100 Subject: [PATCH 02/15] implement 'resource object' class --- README.md | 16 ++- src/Concerns/Identification.php | 15 +- src/Concerns/Implementation.php | 35 +++++ src/Concerns/Relationships.php | 14 +- src/JsonApiResource.php | 78 +++++++++- src/JsonApiResourceCollection.php | 19 ++- src/JsonApiServerImplementation.php | 29 ++++ src/Link.php | 32 +++++ src/Relationship.php | 38 +++++ src/ResourceIdentifier.php | 37 +++++ src/Support/Fields.php | 8 +- src/Support/Includes.php | 4 +- src/{ => Support}/UnknownRelationship.php | 5 +- tests/Unit/AttributesTest.php | 42 ++++++ tests/Unit/JsonApiTest.php | 109 ++++++++++++++ tests/Unit/RelationshipsTest.php | 165 ++++++++++++++++++++++ tests/Unit/ResourceIdentificationTest.php | 8 ++ 17 files changed, 603 insertions(+), 51 deletions(-) create mode 100644 src/Concerns/Implementation.php create mode 100644 src/JsonApiServerImplementation.php create mode 100644 src/Link.php create mode 100644 src/Relationship.php create mode 100644 src/ResourceIdentifier.php rename src/{ => Support}/UnknownRelationship.php (88%) diff --git a/README.md b/README.md index 3015745..f923457 100644 --- a/README.md +++ b/README.md @@ -131,12 +131,14 @@ To provide links for a resource, you can implement the `toLinks(Request $request ```php route('users.show', $this->resource), + 'self' => new Link(route('users.show', $this->resource)), ]; } } @@ -330,22 +332,21 @@ Relationships can be resolved deeply and also multiple relationship paths can be ## Credits - [Tim MacDonald](https://github.com/timacdonald) -- [Jess Archer](https://github.com/jessarcher) for co-creating our initial in-house version +- [Jess Archer](https://github.com/jessarcher) for co-creating our initial in-house version and the brainstorming - [All Contributors](../../contributors) And a special (vegi) thanks to [Caneco](https://twitter.com/caneco) for the logo ✨ # Coming soon... -- [ ] Top level links, jsonapi, etc. +- [ ] Top level links - how would you modify this for a collection? + - [ ] decide how to handle top level keys for single and collections (static? should collections have to be extended to specify the values? or can there be static methods on the single resource for the collection?) - [ ] Test assertions? -- [ ] decide how to handle top level keys for single and collections (static? should collections have to be extended to specify the values? or can there be static methods on the single resource for the collection?) - [ ] Handle loading relations on a already in memory object with Spatie Query builder (PR) - [ ] Resource identifier links and meta as a new concept different to normal resource links and relationships. -- [ ] Ability to send the resource identifier "id" and "type" for a belongsTo relationship, even if not included? -- [ ] Helper to define links - [ ] Investigate collection count support - [ ] Transducers for all the looping? +- [ ] a contract that other classes can implement to support the JSON:API spec as relationships? Can we have it work at a top level as well? Would that even make sense? Maybe be providing a toResponse implementation? # To document @@ -357,3 +358,6 @@ And a special (vegi) thanks to [Caneco](https://twitter.com/caneco) for the logo - [ ] caching id and type - [ ] caching includes and fields - [ ] how it clears itself on toResponse + - [ ] asRelationship() + - [ ] that the goal is to have a consistent output at all levels, hence the maximal dataset for empty values + - [ ] Link object and meta diff --git a/src/Concerns/Identification.php b/src/Concerns/Identification.php index bea03e7..8bd4312 100644 --- a/src/Concerns/Identification.php +++ b/src/Concerns/Identification.php @@ -51,25 +51,12 @@ public static function resolveTypeNormally(): void self::$typeResolver = null; } - /** - * @internal - */ - public function toResourceIdentifier(Request $request): array - { - return [ - 'data' => [ - 'id' => $this->resolveId($request), - 'type' => $this->resolveType($request), - ], - ]; - } - /** * @internal */ public function toUniqueResourceIdentifier(Request $request): string { - return "type:{$this->resolveType($request)} id:{$this->resolveId($request)}"; + return "type:{$this->resolveType($request)};id:{$this->resolveId($request)};"; } /** diff --git a/src/Concerns/Implementation.php b/src/Concerns/Implementation.php new file mode 100644 index 0000000..e7318cf --- /dev/null +++ b/src/Concerns/Implementation.php @@ -0,0 +1,35 @@ + new ServerImplementation('1.0'); + } +} diff --git a/src/Concerns/Relationships.php b/src/Concerns/Relationships.php index 8dd493a..fcedc8b 100644 --- a/src/Concerns/Relationships.php +++ b/src/Concerns/Relationships.php @@ -11,7 +11,7 @@ use TiMacDonald\JsonApi\JsonApiResource; use TiMacDonald\JsonApi\JsonApiResourceCollection; use TiMacDonald\JsonApi\Support\Includes; -use TiMacDonald\JsonApi\UnknownRelationship; +use TiMacDonald\JsonApi\Support\UnknownRelationship; /** * @internal @@ -87,7 +87,7 @@ private function requestedRelationshipsAsIdentifiers(Request $request): Collecti * @param JsonApiResource|JsonApiResourceCollection|UnknownRelationship $resource * @return mixed */ - fn ($resource) => $resource->toResourceIdentifier($request) + fn ($resource) => $resource->asRelationship($request) ); } @@ -102,15 +102,11 @@ private function requestedRelationships(Request $request): Collection /** * @return JsonApiResource|JsonApiResourceCollection|UnknownRelationship */ - function (Closure $value, string $key) use ($request) { + function (Closure $value, string $prefix) use ($request) { $resource = $value(); - if ($resource instanceof JsonApiResource) { - return $resource->withIncludePrefix($key); - } - - if ($resource instanceof JsonApiResourceCollection) { - return $resource->filterDuplicates($request)->withIncludePrefix($key); + if ($resource instanceof JsonApiResource || $resource instanceof JsonApiResourceCollection) { + return $resource->initialiseAsRelationship($request, $prefix); } return new UnknownRelationship($resource); diff --git a/src/JsonApiResource.php b/src/JsonApiResource.php index d246e7d..7a7b170 100644 --- a/src/JsonApiResource.php +++ b/src/JsonApiResource.php @@ -14,62 +14,121 @@ abstract class JsonApiResource extends JsonResource { use Concerns\Attributes; - use Concerns\Relationships; use Concerns\Identification; + use Concerns\Implementation; + use Concerns\Relationships; + /** + * @see https://github.com/timacdonald/json-api#customising-the-resource-id + * @see https://jsonapi.org/format/#document-resource-object-identification + */ public static function resolveIdUsing(Closure $resolver): void { self::$idResolver = $resolver; } + /** + * @see https://github.com/timacdonald/json-api#customising-the-resource-type + * @see https://jsonapi.org/format/#document-resource-object-identification + */ public static function resolveTypeUsing(Closure $resolver): void { self::$typeResolver = $resolver; } + /** + * TODO see local-docs + * @see https://jsonapi.org/format/#document-jsonapi-object + */ + public static function resolveServerImplementationUsing(Closure $resolver): void + { + self::$serverImplementationResolver = $resolver; + } + + /** + * @see https://github.com/timacdonald/json-api#minimal-resource-attributes + */ public static function minimalAttributes(): void { self::$minimalAttributes = true; } + /** + * @see https://github.com/timacdonald/json-api#resource-attributes + * @see https://jsonapi.org/format/#document-resource-object-attributes + */ protected function toAttributes(Request $request): array { return [ - // + // 'name' => $this->name, + // 'address' => fn () => new Address($this->address), ]; } + /** + * @see https://github.com/timacdonald/json-api#resource-relationships + * @see https://jsonapi.org/format/#document-resource-object-relationships + */ protected function toRelationships(Request $request): array { return [ - // + // 'posts' => fn () => PostResource::collection($this->posts), + // 'avatar' => fn () => AvatarResource::make($this->avatar), ]; } + /** + * @see https://github.com/timacdonald/json-api#resource-links + * @see https://jsonapi.org/format/#document-resource-object-links + */ protected function toLinks(Request $request): array { return [ - // + // 'repo' => new Link('https://github.com/timacdonald/json-api'), ]; } + /** + * @see https://github.com/timacdonald/json-api#resource-meta + * @see https://jsonapi.org/format/#document-meta + */ protected function toMeta(Request $request): array { return [ - // + // 'resourceDeprecated' => false, ]; } + /** + * @see https://github.com/timacdonald/json-api#customising-the-resource-id + * @see https://jsonapi.org/format/#document-resource-object-identification + */ protected function toId(Request $request): string { return self::idResolver()($this->resource, $request); } + /** + * @see https://github.com/timacdonald/json-api#customising-the-resource-type + * @see https://jsonapi.org/format/#document-resource-object-identification + */ protected function toType(Request $request): string { return self::typeResolver()($this->resource, $request); } + /** + * TODO: @see docs-link + * @see https://jsonapi.org/format/#document-resource-object-linkage + */ + public function asRelationship(Request $request): Relationship + { + // ??? + return new Relationship( + new ResourceIdentifier($this->resolveId($request), $this->resolveType($request)) + ); + } + /** * @param Request $request */ @@ -92,6 +151,7 @@ public function with($request): array { return [ 'included' => $this->included($request), + 'jsonapi' => self::serverImplementationResolver()($request), ]; } @@ -115,4 +175,12 @@ public function toResponse($request): JsonResponse { return tap(parent::toResponse($request)->header('Content-type', 'application/vnd.api+json'), fn () => Cache::flush($this)); } + + /** + * @internal + */ + public function initialiseAsRelationship(Request $request, string $prefix): self + { + return $this->withIncludePrefix($prefix); + } } diff --git a/src/JsonApiResourceCollection.php b/src/JsonApiResourceCollection.php index 9cf6bd5..df6af7b 100644 --- a/src/JsonApiResourceCollection.php +++ b/src/JsonApiResourceCollection.php @@ -22,6 +22,7 @@ public function with($request): array ->flatten() ->reject(fn (?JsonApiResource $resource): bool => $resource === null) ->uniqueStrict(fn (JsonApiResource $resource): string => $resource->toUniqueResourceIdentifier($request)), + 'jsonapi' => JsonApiResource::serverImplementationResolver()($request), ]; } @@ -35,9 +36,8 @@ public function toResponse($request) /** * @internal - * @return static */ - public function withIncludePrefix(string $prefix) + public function withIncludePrefix(string $prefix): self { /** @phpstan-ignore-next-line */ $this->collection->each(fn (JsonApiResource $resource): JsonApiResource => $resource->withIncludePrefix($prefix)); @@ -56,9 +56,9 @@ public function included(Request $request): Collection /** * @internal */ - public function toResourceIdentifier(Request $request): array + public function asRelationship(Request $request): Collection { - return $this->collection->map(fn (JsonApiResource $resource): array => $resource->toResourceIdentifier($request))->all(); + return $this->collection->map(fn (JsonApiResource $resource): Relationship => $resource->asRelationship($request)); } /** @@ -71,9 +71,8 @@ public function includable(): Collection /** * @internal - * @return static */ - public function filterDuplicates(Request $request) + public function filterDuplicates(Request $request): self { $this->collection = $this->collection->uniqueStrict(fn (JsonApiResource $resource): string => $resource->toUniqueResourceIdentifier($request)); @@ -88,4 +87,12 @@ public function flush(): void { $this->collection->each(fn (JsonApiResource $resource) => $resource->flush()); } + + /** + * @internal + */ + public function initialiseAsRelationship(Request $request, string $prefix): self + { + return $this->withIncludePrefix($prefix)->filterDuplicates($request); + } } diff --git a/src/JsonApiServerImplementation.php b/src/JsonApiServerImplementation.php new file mode 100644 index 0000000..4a798f0 --- /dev/null +++ b/src/JsonApiServerImplementation.php @@ -0,0 +1,29 @@ +version = $version; + + $this->meta = $meta; + } + + public function jsonSerialize(): array + { + return [ + 'version' => $this->version, + 'meta' => (object) $this->meta, + ]; + } +} diff --git a/src/Link.php b/src/Link.php new file mode 100644 index 0000000..cc93b37 --- /dev/null +++ b/src/Link.php @@ -0,0 +1,32 @@ +href = $href; + + $this->meta = $meta; + } + + public function jsonSerialize(): array + { + return [ + 'href' => $this->href, + 'meta' => (object) $this->meta, + ]; + } +} diff --git a/src/Relationship.php b/src/Relationship.php new file mode 100644 index 0000000..04acf0c --- /dev/null +++ b/src/Relationship.php @@ -0,0 +1,38 @@ +data = $data; + + $this->links = $links; + + $this->meta = $meta; + } + + public function jsonSerialize(): array + { + return [ + 'data' => $this->data ?? new stdClass(), + 'meta' => (object) $this->meta, + 'links' => (object) $this->links, + ]; + } +} diff --git a/src/ResourceIdentifier.php b/src/ResourceIdentifier.php new file mode 100644 index 0000000..28ceb1a --- /dev/null +++ b/src/ResourceIdentifier.php @@ -0,0 +1,37 @@ +id = $id; + + $this->type = $type; + + $this->meta = $meta; + } + + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'type' => $this->type, + 'meta' => (object) $this->meta, + ]; + } +} diff --git a/src/Support/Fields.php b/src/Support/Fields.php index ffcd159..9729e90 100644 --- a/src/Support/Fields.php +++ b/src/Support/Fields.php @@ -34,9 +34,7 @@ public function parse(Request $request, string $resourceType): ?array return $this->rememberResourceType($resourceType, function () use ($request, $resourceType): ?array { $typeFields = $request->query('fields') ?? []; - if (is_string($typeFields)) { - abort(400, 'The fields parameter must be an array of resource types.'); - } + abort_if(is_string($typeFields), 400, 'The fields parameter must be an array of resource types.'); if (! array_key_exists($resourceType, $typeFields)) { return null; @@ -48,9 +46,7 @@ public function parse(Request $request, string $resourceType): ?array return []; } - if (! is_string($fields)) { - abort(400, 'The fields parameter value must be a comma seperated list of attributes.'); - } + abort_if(! is_string($fields), 400, 'The fields parameter value must be a comma seperated list of attributes.'); return array_filter(explode(',', $fields), fn (string $value): bool => $value !== ''); }); diff --git a/src/Support/Includes.php b/src/Support/Includes.php index 331133c..2ef25b6 100644 --- a/src/Support/Includes.php +++ b/src/Support/Includes.php @@ -36,9 +36,7 @@ public function parse(Request $request, string $prefix): Collection return $this->rememberIncludes($prefix, function () use ($request, $prefix): Collection { $includes = $request->query('include') ?? ''; - if (is_array($includes)) { - abort(400, 'The include parameter must be a comma seperated list of relationship paths.'); - } + abort_if(is_array($includes), 400, 'The include parameter must be a comma seperated list of relationship paths.'); return Collection::make(explode(',', $includes)) ->when($prefix !== '', function (Collection $includes) use ($prefix): Collection { diff --git a/src/UnknownRelationship.php b/src/Support/UnknownRelationship.php similarity index 88% rename from src/UnknownRelationship.php rename to src/Support/UnknownRelationship.php index ed83113..636637f 100644 --- a/src/UnknownRelationship.php +++ b/src/Support/UnknownRelationship.php @@ -2,8 +2,9 @@ declare(strict_types=1); -namespace TiMacDonald\JsonApi; +namespace TiMacDonald\JsonApi\Support; +use Illuminate\Http\Request; use Illuminate\Http\Resources\PotentiallyMissing; use Illuminate\Support\Collection; @@ -28,7 +29,7 @@ public function __construct($resource) /** * @return mixed */ - public function toResourceIdentifier() + public function asRelationship(Request $request) { return $this->resource; } diff --git a/tests/Unit/AttributesTest.php b/tests/Unit/AttributesTest.php index 2cecab4..fb71067 100644 --- a/tests/Unit/AttributesTest.php +++ b/tests/Unit/AttributesTest.php @@ -46,6 +46,10 @@ protected function toAttributes(Request $request): array 'meta' => [], 'links' => [], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [], ]); } @@ -84,6 +88,10 @@ protected function toAttributes(Request $request): array 'links' => [], 'meta' => [], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [], ]); } @@ -119,6 +127,10 @@ protected function toAttributes(Request $request): array 'meta' => [], 'links' => [], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [], ]); } @@ -152,6 +164,10 @@ protected function toAttributes(Request $request): array 'links' => [], 'meta' => [], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [], ]); } @@ -184,6 +200,10 @@ protected function toAttributes(Request $request): array 'links' => [], 'meta' => [], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [], ]); } @@ -236,6 +256,10 @@ public function testItCanSpecifyMinimalAttributes(): void 'meta' => [], 'links' => [], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [], ]); @@ -275,19 +299,29 @@ public function testItCanUseSparseFieldsetsWithIncludedCollections(): void 'data' => [ 'id' => 'post-id-1', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], [ 'data' => [ 'id' => 'post-id-2', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], ], ], 'meta' => [], 'links' => [], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [ [ 'id' => 'post-id-1', @@ -346,6 +380,10 @@ protected function toAttributes(Request $request): array 'meta' => [], 'links' => [], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [], ]); } @@ -385,6 +423,10 @@ protected function toAttributes(Request $request): array 'meta' => [], 'links' => [], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [], ]); } diff --git a/tests/Unit/JsonApiTest.php b/tests/Unit/JsonApiTest.php index c920a35..bacccc9 100644 --- a/tests/Unit/JsonApiTest.php +++ b/tests/Unit/JsonApiTest.php @@ -10,7 +10,11 @@ use Tests\Resources\BasicJsonApiResource; use Tests\Resources\UserResource; use Tests\TestCase; +use TiMacDonald\JsonApi\JsonApiServerImplementation; use TiMacDonald\JsonApi\JsonApiResource; +use TiMacDonald\JsonApi\Link; +use TiMacDonald\JsonApi\Relationship; +use TiMacDonald\JsonApi\ResourceIdentifier; use TiMacDonald\JsonApi\Support\Fields; use TiMacDonald\JsonApi\Support\Includes; use function get_class; @@ -40,6 +44,10 @@ public function testItCanReturnASingleResource(): void 'links' => [], ], 'included' => [], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], ]); } @@ -84,6 +92,10 @@ public function testItCanReturnACollection(): void ], ], 'included' => [], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], ]); } @@ -122,6 +134,10 @@ protected function toMeta(Request $request): array 'links' => [], ], 'included' => [], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], ]); } @@ -151,6 +167,10 @@ protected function toLinks(Request $request): array ], ], 'included' => [], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], ]); } @@ -189,6 +209,10 @@ public function testItCanCustomiseTheTypeResolution(): void 'links' => [], ], 'included' => [], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], ]); JsonApiResource::resolveTypeNormally(); @@ -211,6 +235,10 @@ public function testItCanCustomiseTheIdResolution(): void 'links' => [], ], 'included' => [], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], ]); JsonApiResource::resolveIdNormally(); @@ -232,6 +260,10 @@ public function testItClearsTheHelperCachesAfterPreparingResponseForASingleResou 'links' => [], ], 'included' => [], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], ]); $this->assertCount(0, Fields::getInstance()->cache()); $this->assertCount(0, Includes::getInstance()->cache()); @@ -255,8 +287,85 @@ public function testItClearsTheHelperCachesAfterPreparingResponseForACollectionO ], ], 'included' => [], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], ]); $this->assertCount(0, Fields::getInstance()->cache()); $this->assertCount(0, Includes::getInstance()->cache()); } + + public function testItCastsEmptyRelationshipsAttributesToObjects(): void + { + $relationship = new Relationship(null, [], []); + + $json = json_encode($relationship); + + self::assertSame('{"data":{},"meta":{},"links":{}}', $json); + } + + public function testItCastsEmptyResourceIdentifierMetaToObject(): void + { + $relationship = new ResourceIdentifier('5', 'users'); + + $json = json_encode($relationship); + + self::assertSame('{"id":"5","type":"users","meta":{}}', $json); + } + + public function testItCastsEmptyLinksMetaToObject(): void + { + $link = new Link('https://timacdonald.me', []); + + $json = json_encode($link); + + self::assertSame('{"href":"https:\/\/timacdonald.me","meta":{}}', $json); + } + + public function testItCastsEmptyImplementationMetaToObject(): void + { + $implementation = new JsonApiServerImplementation('1.5', []); + + $json = json_encode($implementation); + + self::assertSame('{"version":"1.5","meta":{}}', $json); + } + + public function testItCanSpecifyAnImplementation(): void + { + BasicJsonApiResource::resolveServerImplementationUsing(fn () => new JsonApiServerImplementation('1.4.3', [ + 'secure' => true, + ])); + $user = new BasicModel([ + 'id' => 'user-id', + 'name' => 'user-name', + ]); + Route::get('test-route', fn () => UserResource::make($user)); + + $response = $this->getJson('test-route'); + + $response->assertOk(); + $response->assertExactJson([ + 'data' => [ + 'id' => 'user-id', + 'type' => 'basicModels', + 'attributes' => [ + 'name' => 'user-name', + ], + 'relationships' => [], + 'meta' => [], + 'links' => [], + ], + 'included' => [], + 'jsonapi' => [ + 'version' => '1.4.3', + 'meta' => [ + 'secure' => true, + ], + ], + ]); + + BasicJsonApiResource::resolveServerImplementationNormally(); + } } diff --git a/tests/Unit/RelationshipsTest.php b/tests/Unit/RelationshipsTest.php index c68b46a..8e1466d 100644 --- a/tests/Unit/RelationshipsTest.php +++ b/tests/Unit/RelationshipsTest.php @@ -64,6 +64,10 @@ protected function toRelationships(Request $request): array 'meta' => [], ], 'included' => [], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], ]); } @@ -96,12 +100,19 @@ public function testItCanIncludeASingleToOneResourceForASingleResource(): void 'data' => [ 'id' => 'author-id', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], ], 'links' => [], 'meta' => [], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [ [ 'id' => 'author-id', @@ -158,18 +169,28 @@ public function testItCanIncludeNestedToOneResourcesForASingleResource(): void 'data' => [ 'id' => 'author-id', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], 'featureImage' => [ 'data' => [ 'id' => 'feature-image-id', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], ], 'links' => [], 'meta' => [], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [ [ 'id' => 'author-id', @@ -182,13 +203,19 @@ public function testItCanIncludeNestedToOneResourcesForASingleResource(): void 'data' => [ 'id' => 'avatar-id', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], 'license' => [ 'data' => [ 'id' => 'license-id', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], ], 'links' => [], @@ -266,12 +293,19 @@ public function toRelationships(Request $request): array 'data' => [ 'id' => 'child-id-1', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], ], 'links' => [], 'meta' => [], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [ [ 'id' => 'child-id-1', @@ -282,7 +316,10 @@ public function toRelationships(Request $request): array 'data' => [ 'id' => 'child-id-2', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], ], 'links' => [], @@ -339,12 +376,19 @@ public function toRelationships(Request $request): array 'data' => [ 'id' => 'child-id-1', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], ], 'meta' => [], 'links' => [], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [ [ 'id' => 'child-id-1', @@ -356,13 +400,19 @@ public function toRelationships(Request $request): array 'data' => [ 'id' => 'child-id-2', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], [ 'data' => [ 'id' => 'child-id-3', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], ], ], @@ -428,7 +478,10 @@ public function testItCanIncludeToOneResourcesForACollectionOfResources(): void 'data' => [ 'id' => 'author-id-1', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], ], 'meta' => [], @@ -446,13 +499,20 @@ public function testItCanIncludeToOneResourcesForACollectionOfResources(): void 'data' => [ 'id' => 'author-id-2', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], ], 'meta' => [], 'links' => [], ], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [ [ 'id' => 'author-id-1', @@ -514,19 +574,29 @@ public function testItCanIncludeACollectionOfResourcesForASingleResource(): void 'data' => [ 'id' => 'post-id-1', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], [ 'data' => [ 'id' => 'post-id-2', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], ], ], 'meta' => [], 'links' => [], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [ [ 'id' => 'post-id-1', @@ -634,13 +704,19 @@ public function testItCanIncludeAManyManyManyRelationship(): void 'data' => [ 'id' => 'comment-id-1', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], [ 'data' => [ 'id' => 'comment-id-2', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], ], ], @@ -660,13 +736,19 @@ public function testItCanIncludeAManyManyManyRelationship(): void 'data' => [ 'id' => 'comment-id-3', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], [ 'data' => [ 'id' => 'comment-id-4', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], ], ], @@ -674,6 +756,10 @@ public function testItCanIncludeAManyManyManyRelationship(): void 'links' => [], ], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [ [ 'id' => 'comment-id-1', @@ -687,13 +773,19 @@ public function testItCanIncludeAManyManyManyRelationship(): void 'data' => [ 'id' => 'like-id-1', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], [ 'data' => [ 'id' => 'like-id-2', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], ], ], @@ -712,13 +804,19 @@ public function testItCanIncludeAManyManyManyRelationship(): void 'data' => [ 'id' => 'like-id-3', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], [ 'data' => [ 'id' => 'like-id-4', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], ], ], @@ -769,13 +867,19 @@ public function testItCanIncludeAManyManyManyRelationship(): void 'data' => [ 'id' => 'like-id-5', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], [ 'data' => [ 'id' => 'like-id-6', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], ], ], @@ -794,13 +898,19 @@ public function testItCanIncludeAManyManyManyRelationship(): void 'data' => [ 'id' => 'like-id-7', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], [ 'data' => [ 'id' => 'like-id-8', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], ], ], @@ -889,12 +999,19 @@ protected function toAttributes(Request $request): array 'data' => [ 'id' => 'relation-id', 'type' => 'relation-type', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], ], 'meta' => [], 'links' => [], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [ [ 'id' => 'relation-id', @@ -946,7 +1063,10 @@ public function testItFiltersOutDuplicateIncludesForACollectionOfResources(): vo 'data' => [ 'id' => 'avatar-id', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], ], 'meta' => [], @@ -963,13 +1083,20 @@ public function testItFiltersOutDuplicateIncludesForACollectionOfResources(): vo 'data' => [ 'id' => 'avatar-id', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], ], 'meta' => [], 'links' => [], ], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [ [ 'id' => 'avatar-id', @@ -1020,13 +1147,20 @@ public function testItFiltersOutDuplicateIncludesForASingleResource(): void 'data' => [ 'id' => 'post-id', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], ], ], 'meta' => [], 'links' =>[], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [ [ 'id' => 'post-id', @@ -1067,6 +1201,10 @@ public function testItHasIncludedArrayWhenIncludeParameterIsPresentForASingleRes 'meta' => [], 'links' =>[], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [], ]); } @@ -1097,6 +1235,10 @@ public function testItHasIncludedArrayWhenIncludeParameterIsPresentForACollectio 'links' => [], ], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [], ]); } @@ -1124,6 +1266,10 @@ public function testItCanReturnNullForEmptyToOneRelationships(): void 'meta' => [], 'links' => [], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [], ]); } @@ -1151,6 +1297,10 @@ public function testItCanReturnAnEmptyArrayForEmptyToManyRelationships(): void 'meta' => [], 'links' => [], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [], ]); } @@ -1209,12 +1359,19 @@ public function toRelationships(Request $request): array 'data' => [ 'id' => '2', 'type' => 'basicModels', + 'meta' => [], ], + 'links' => [], + 'meta' => [], ], ], 'meta' => [], 'links' => [], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [ [ 'id' => '2', @@ -1262,6 +1419,10 @@ public function toRelationships(Request $request): array 'meta' => [], 'links' => [], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [], ]); } @@ -1298,6 +1459,10 @@ public function toRelationships(Request $request): array 'meta' => [], 'links' => [], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [], ]); } diff --git a/tests/Unit/ResourceIdentificationTest.php b/tests/Unit/ResourceIdentificationTest.php index 59429d3..c4c6722 100644 --- a/tests/Unit/ResourceIdentificationTest.php +++ b/tests/Unit/ResourceIdentificationTest.php @@ -33,6 +33,10 @@ public function testItResolvesTheIdAndTypeOfAModel(): void 'links' => [], 'meta' => [], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [], ]); } @@ -58,6 +62,10 @@ public function testItCastsAModelsIntegerIdToAString(): void 'links' => [], 'meta' => [], ], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], 'included' => [], ]); } From ad3e744965a8cfc6d196c7e2e04cd569299b56b1 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Sat, 18 Dec 2021 16:09:21 +1100 Subject: [PATCH 03/15] wip --- README.md | 2 +- src/Concerns/Attributes.php | 18 +---------- src/Concerns/Links.php | 33 ++++++++++++++++++++ src/Concerns/Relationships.php | 2 +- src/JsonApiResource.php | 21 ++++++------- src/JsonApiResourceCollection.php | 19 ------------ src/JsonApiServerImplementation.php | 3 ++ src/Link.php | 28 ++++++++++++++++- src/Relationship.php | 3 ++ src/ResourceIdentifier.php | 3 ++ src/Support/Fields.php | 8 +++-- tests/Unit/JsonApiTest.php | 48 +++++++++++++++++++++++++++-- tests/Unit/RelationshipsTest.php | 11 ++++++- 13 files changed, 142 insertions(+), 57 deletions(-) create mode 100644 src/Concerns/Links.php diff --git a/README.md b/README.md index f923457..1706cb3 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ class UserResource extends JsonApiResource protected function toLinks(Request $request): array { return [ - 'self' => new Link(route('users.show', $this->resource)), + Link::self(route('users.show', $this->resource)), ]; } } diff --git a/src/Concerns/Attributes.php b/src/Concerns/Attributes.php index 1406225..715d45d 100644 --- a/src/Concerns/Attributes.php +++ b/src/Concerns/Attributes.php @@ -34,7 +34,7 @@ public static function maximalAttributes(): void private function requestedAttributes(Request $request): Collection { return Collection::make($this->toAttributes($request)) - ->only($this->fields($request)) + ->only(Fields::getInstance()->parse($request, $this->toType($request), self::$minimalAttributes)) ->map( /** * @param mixed $value @@ -49,20 +49,4 @@ private function requestedAttributes(Request $request): Collection fn ($value): bool => $value instanceof PotentiallyMissing && $value->isMissing() ); } - - /** - * @internal - */ - private function fields(Request $request): ?array - { - $fields = Fields::getInstance()->parse($request, $this->toType($request)); - - if ($fields !== null) { - return $fields; - } - - return self::$minimalAttributes - ? [] - : null; - } } diff --git a/src/Concerns/Links.php b/src/Concerns/Links.php new file mode 100644 index 0000000..6091dcb --- /dev/null +++ b/src/Concerns/Links.php @@ -0,0 +1,33 @@ +toLinks($request)) + ->mapWithKeys( + /** + * @param mixed $value + * @param int|string $key + */ + fn ($value, $key): array => $value instanceof Link + ? [$value->key() => $value] + : [$key => $value] + ) + ->all(); + } +} diff --git a/src/Concerns/Relationships.php b/src/Concerns/Relationships.php index fcedc8b..325f97d 100644 --- a/src/Concerns/Relationships.php +++ b/src/Concerns/Relationships.php @@ -106,7 +106,7 @@ function (Closure $value, string $prefix) use ($request) { $resource = $value(); if ($resource instanceof JsonApiResource || $resource instanceof JsonApiResourceCollection) { - return $resource->initialiseAsRelationship($request, $prefix); + return $resource->withIncludePrefix($prefix); } return new UnknownRelationship($resource); diff --git a/src/JsonApiResource.php b/src/JsonApiResource.php index 7a7b170..18005de 100644 --- a/src/JsonApiResource.php +++ b/src/JsonApiResource.php @@ -16,6 +16,7 @@ abstract class JsonApiResource extends JsonResource use Concerns\Attributes; use Concerns\Identification; use Concerns\Implementation; + use Concerns\Links; use Concerns\Relationships; /** @@ -61,6 +62,9 @@ protected function toAttributes(Request $request): array { return [ // 'name' => $this->name, + // + // or with lazy evaluation... + // // 'address' => fn () => new Address($this->address), ]; } @@ -84,7 +88,8 @@ protected function toRelationships(Request $request): array protected function toLinks(Request $request): array { return [ - // 'repo' => new Link('https://github.com/timacdonald/json-api'), + // Link::self(route('users.show'), $this->resource), + // Link::related(/** ... */), ]; } @@ -123,7 +128,6 @@ protected function toType(Request $request): string */ public function asRelationship(Request $request): Relationship { - // ??? return new Relationship( new ResourceIdentifier($this->resolveId($request), $this->resolveType($request)) ); @@ -140,7 +144,7 @@ public function toArray($request): array 'attributes' => (object) $this->requestedAttributes($request)->all(), 'relationships' => (object) $this->requestedRelationshipsAsIdentifiers($request)->all(), 'meta' => (object) $this->toMeta($request), - 'links' => (object) $this->toLinks($request), + 'links' => (object) $this->resolveLinks($request), ]; } @@ -150,7 +154,8 @@ public function toArray($request): array public function with($request): array { return [ - 'included' => $this->included($request), + 'included' => $this->included($request) + ->uniqueStrict(fn (JsonApiResource $resource): string => $resource->toUniqueResourceIdentifier($request)), 'jsonapi' => self::serverImplementationResolver()($request), ]; } @@ -175,12 +180,4 @@ public function toResponse($request): JsonResponse { return tap(parent::toResponse($request)->header('Content-type', 'application/vnd.api+json'), fn () => Cache::flush($this)); } - - /** - * @internal - */ - public function initialiseAsRelationship(Request $request, string $prefix): self - { - return $this->withIncludePrefix($prefix); - } } diff --git a/src/JsonApiResourceCollection.php b/src/JsonApiResourceCollection.php index df6af7b..22160a1 100644 --- a/src/JsonApiResourceCollection.php +++ b/src/JsonApiResourceCollection.php @@ -20,7 +20,6 @@ public function with($request): array 'included' => $this->collection ->map(fn (JsonApiResource $resource): Collection => $resource->included($request)) ->flatten() - ->reject(fn (?JsonApiResource $resource): bool => $resource === null) ->uniqueStrict(fn (JsonApiResource $resource): string => $resource->toUniqueResourceIdentifier($request)), 'jsonapi' => JsonApiResource::serverImplementationResolver()($request), ]; @@ -69,16 +68,6 @@ public function includable(): Collection return $this->collection; } - /** - * @internal - */ - public function filterDuplicates(Request $request): self - { - $this->collection = $this->collection->uniqueStrict(fn (JsonApiResource $resource): string => $resource->toUniqueResourceIdentifier($request)); - - return $this; - } - /** * @internal * @infection-ignore-all @@ -87,12 +76,4 @@ public function flush(): void { $this->collection->each(fn (JsonApiResource $resource) => $resource->flush()); } - - /** - * @internal - */ - public function initialiseAsRelationship(Request $request, string $prefix): self - { - return $this->withIncludePrefix($prefix)->filterDuplicates($request); - } } diff --git a/src/JsonApiServerImplementation.php b/src/JsonApiServerImplementation.php index 4a798f0..d1b89a7 100644 --- a/src/JsonApiServerImplementation.php +++ b/src/JsonApiServerImplementation.php @@ -19,6 +19,9 @@ public function __construct(string $version, array $meta = []) $this->meta = $meta; } + /** + * @internal + */ public function jsonSerialize(): array { return [ diff --git a/src/Link.php b/src/Link.php index cc93b37..adcfcd0 100644 --- a/src/Link.php +++ b/src/Link.php @@ -15,13 +15,28 @@ class Link implements JsonSerializable private array $meta; - public function __construct(string $href, array $meta = []) + private string $key = 'unknown'; + + public static function self(string $href, array $meta = []): self + { + return tap(new self($href, $meta), fn (self $instance) => $instance->key = 'self'); + } + + public static function related(string $href, array $meta = []): self + { + return tap(new self($href, $meta), fn (self $instance) => $instance->key = 'related'); + } + + private function __construct(string $href, array $meta = []) { $this->href = $href; $this->meta = $meta; } + /** + * @internal + */ public function jsonSerialize(): array { return [ @@ -29,4 +44,15 @@ public function jsonSerialize(): array 'meta' => (object) $this->meta, ]; } + + /** + * @internal + */ + public function key(): string + { + return [ + 'self' => 'self', + 'related' => 'related', + ][$this->key]; + } } diff --git a/src/Relationship.php b/src/Relationship.php index 04acf0c..7a224bc 100644 --- a/src/Relationship.php +++ b/src/Relationship.php @@ -27,6 +27,9 @@ public function __construct(?ResourceIdentifier $data = null, array $links = [], $this->meta = $meta; } + /** + * @internal + */ public function jsonSerialize(): array { return [ diff --git a/src/ResourceIdentifier.php b/src/ResourceIdentifier.php index 28ceb1a..a7413a0 100644 --- a/src/ResourceIdentifier.php +++ b/src/ResourceIdentifier.php @@ -26,6 +26,9 @@ public function __construct(string $id, string $type, array $meta = []) $this->meta = $meta; } + /** + * @internal + */ public function jsonSerialize(): array { return [ diff --git a/src/Support/Fields.php b/src/Support/Fields.php index 9729e90..3a13459 100644 --- a/src/Support/Fields.php +++ b/src/Support/Fields.php @@ -29,15 +29,17 @@ public static function getInstance(): self return self::$instance ??= new self(); } - public function parse(Request $request, string $resourceType): ?array + public function parse(Request $request, string $resourceType, bool $minimalAttributes = false): ?array { - return $this->rememberResourceType($resourceType, function () use ($request, $resourceType): ?array { + return $this->rememberResourceType("type:{$resourceType};minimal:{$minimalAttributes};", function () use ($request, $resourceType, $minimalAttributes): ?array { $typeFields = $request->query('fields') ?? []; abort_if(is_string($typeFields), 400, 'The fields parameter must be an array of resource types.'); if (! array_key_exists($resourceType, $typeFields)) { - return null; + return $minimalAttributes + ? [] + : null; } $fields = $typeFields[$resourceType]; diff --git a/tests/Unit/JsonApiTest.php b/tests/Unit/JsonApiTest.php index bacccc9..923b2f4 100644 --- a/tests/Unit/JsonApiTest.php +++ b/tests/Unit/JsonApiTest.php @@ -141,7 +141,7 @@ protected function toMeta(Request $request): array ]); } - public function testItAddsLinksToIndividualResources(): void + public function testItAddsArbitraryLinksToIndividualResources(): void { Route::get('test-route', fn () => new class ((new BasicModel(['id' => 'expected-id']))) extends JsonApiResource { protected function toLinks(Request $request): array @@ -174,6 +174,50 @@ protected function toLinks(Request $request): array ]); } + public function testItHandlesSelfAndRelatedLinks(): void + { + Route::get('test-route', fn () => new class ((new BasicModel(['id' => 'expected-id']))) extends JsonApiResource { + protected function toLinks(Request $request): array + { + return [ + Link::self('https://example.test/self'), + Link::related('https://example.test/related'), + 'home' => 'https://example.test', + ]; + } + }); + + $response = $this->getJson('test-route'); + + $response->assertOk(); + $response->assertExactJson([ + 'data' => [ + 'id' => 'expected-id', + 'type' => 'basicModels', + 'attributes' => [], + 'relationships' => [], + 'meta' => [], + 'links' => [ + 'self' => [ + 'href' => 'https://example.test/self', + 'meta' => [], + ], + 'related' => [ + 'href' => 'https://example.test/related', + 'meta' => [], + ], + 'home' => 'https://example.test' + ], + ], + 'included' => [], + 'jsonapi' => [ + 'version' => '1.0', + 'meta' => [], + ], + ]); + + } + public function testItSetsTheContentTypeHeaderForASingleResource(): void { Route::get('test-route', fn () => BasicJsonApiResource::make((new BasicModel(['id' => 'xxxx'])))); @@ -316,7 +360,7 @@ public function testItCastsEmptyResourceIdentifierMetaToObject(): void public function testItCastsEmptyLinksMetaToObject(): void { - $link = new Link('https://timacdonald.me', []); + $link = Link::self('https://timacdonald.me', []); $json = json_encode($link); diff --git a/tests/Unit/RelationshipsTest.php b/tests/Unit/RelationshipsTest.php index 8e1466d..7b84b0e 100644 --- a/tests/Unit/RelationshipsTest.php +++ b/tests/Unit/RelationshipsTest.php @@ -1112,7 +1112,7 @@ public function testItFiltersOutDuplicateIncludesForACollectionOfResources(): vo ]); } - public function testItFiltersOutDuplicateIncludesForASingleResource(): void + public function testItFiltersOutDuplicateResourceObjectsIncludesForASingleResource(): void { $user = (new BasicModel([ 'id' => 'user-id', @@ -1152,6 +1152,15 @@ public function testItFiltersOutDuplicateIncludesForASingleResource(): void 'links' => [], 'meta' => [], ], + [ + 'data' => [ + 'id' => 'post-id', + 'type' => 'basicModels', + 'meta' => [], + ], + 'links' => [], + 'meta' => [], + ], ], ], 'meta' => [], From 1da7e3f6d9e22f20d4e2ebf7b28b15ca8f5ff8c2 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Sat, 18 Dec 2021 16:14:18 +1100 Subject: [PATCH 04/15] wpi --- src/Concerns/Relationships.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Concerns/Relationships.php b/src/Concerns/Relationships.php index 325f97d..5b4bee4 100644 --- a/src/Concerns/Relationships.php +++ b/src/Concerns/Relationships.php @@ -102,7 +102,7 @@ private function requestedRelationships(Request $request): Collection /** * @return JsonApiResource|JsonApiResourceCollection|UnknownRelationship */ - function (Closure $value, string $prefix) use ($request) { + function (Closure $value, string $prefix) { $resource = $value(); if ($resource instanceof JsonApiResource || $resource instanceof JsonApiResourceCollection) { From 0a9800bce2d905977244a2d39d80073b70e8689a Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Sat, 18 Dec 2021 16:16:18 +1100 Subject: [PATCH 05/15] wip --- tests/Unit/JsonApiTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/JsonApiTest.php b/tests/Unit/JsonApiTest.php index 923b2f4..c688b9c 100644 --- a/tests/Unit/JsonApiTest.php +++ b/tests/Unit/JsonApiTest.php @@ -10,8 +10,8 @@ use Tests\Resources\BasicJsonApiResource; use Tests\Resources\UserResource; use Tests\TestCase; -use TiMacDonald\JsonApi\JsonApiServerImplementation; use TiMacDonald\JsonApi\JsonApiResource; +use TiMacDonald\JsonApi\JsonApiServerImplementation; use TiMacDonald\JsonApi\Link; use TiMacDonald\JsonApi\Relationship; use TiMacDonald\JsonApi\ResourceIdentifier; From d25c6e6e80601e72e2d5a445784f9d6d8cf2aca1 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Sat, 18 Dec 2021 16:21:47 +1100 Subject: [PATCH 06/15] wip --- tests/Unit/JsonApiTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Unit/JsonApiTest.php b/tests/Unit/JsonApiTest.php index c688b9c..493b67b 100644 --- a/tests/Unit/JsonApiTest.php +++ b/tests/Unit/JsonApiTest.php @@ -206,7 +206,7 @@ protected function toLinks(Request $request): array 'href' => 'https://example.test/related', 'meta' => [], ], - 'home' => 'https://example.test' + 'home' => 'https://example.test', ], ], 'included' => [], @@ -215,7 +215,6 @@ protected function toLinks(Request $request): array 'meta' => [], ], ]); - } public function testItSetsTheContentTypeHeaderForASingleResource(): void From 1f583c273d70661a5725a4cb962f935a21a3c3f5 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Sat, 18 Dec 2021 16:48:23 +1100 Subject: [PATCH 07/15] wip --- tests/Unit/FieldsTest.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/Unit/FieldsTest.php b/tests/Unit/FieldsTest.php index 82bb643..ae56e6c 100644 --- a/tests/Unit/FieldsTest.php +++ b/tests/Unit/FieldsTest.php @@ -37,4 +37,11 @@ public function testItHandlesEmptyValues(): void $this->assertSame([], Fields::getInstance()->parse($request, 'a')); } + + public function testMinimalAttributes(): void + { + $request = Request::create('https://example.com'); + + $this->assertSame([], Fields::getInstance()->parse($request, 'a', true)); + } } From bf111e447b4373ac1ac4e6af58d3d3a2d3d1bf5b Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Sat, 18 Dec 2021 16:53:38 +1100 Subject: [PATCH 08/15] wip --- src/Support/Fields.php | 2 +- tests/Performance/FieldsTest.php | 2 +- tests/Unit/FieldsTest.php | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Support/Fields.php b/src/Support/Fields.php index 3a13459..9d5a653 100644 --- a/src/Support/Fields.php +++ b/src/Support/Fields.php @@ -29,7 +29,7 @@ public static function getInstance(): self return self::$instance ??= new self(); } - public function parse(Request $request, string $resourceType, bool $minimalAttributes = false): ?array + public function parse(Request $request, string $resourceType, bool $minimalAttributes): ?array { return $this->rememberResourceType("type:{$resourceType};minimal:{$minimalAttributes};", function () use ($request, $resourceType, $minimalAttributes): ?array { $typeFields = $request->query('fields') ?? []; diff --git a/tests/Performance/FieldsTest.php b/tests/Performance/FieldsTest.php index e6b90b1..8de9667 100644 --- a/tests/Performance/FieldsTest.php +++ b/tests/Performance/FieldsTest.php @@ -34,7 +34,7 @@ $start = microtime(true); foreach ($resources as $resource) { - Fields::getInstance()->parse($request, $resource); + Fields::getInstance()->parse($request, $resource, true); } $end = microtime(true); diff --git a/tests/Unit/FieldsTest.php b/tests/Unit/FieldsTest.php index ae56e6c..f3a66a7 100644 --- a/tests/Unit/FieldsTest.php +++ b/tests/Unit/FieldsTest.php @@ -23,8 +23,8 @@ public function testItHandlesMultipleRequests(): void ]; $fields = [ - Fields::getInstance()->parse($requests[0], 'a'), - Fields::getInstance()->parse($requests[1], 'b'), + Fields::getInstance()->parse($requests[0], 'a', true), + Fields::getInstance()->parse($requests[1], 'b', true), ]; $this->assertSame(['a'], $fields[0]); @@ -35,10 +35,10 @@ public function testItHandlesEmptyValues(): void { $request = Request::create('https://example.com?fields[a]='); - $this->assertSame([], Fields::getInstance()->parse($request, 'a')); + $this->assertSame([], Fields::getInstance()->parse($request, 'a', true)); } - public function testMinimalAttributes(): void + public function testItProvidesMinimalAttributesWhenNoFieldsAreSpecified(): void { $request = Request::create('https://example.com'); From e9d0cf137408a939fcb979e6e0bda20756e1e821 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Sat, 18 Dec 2021 17:34:33 +1100 Subject: [PATCH 09/15] wip --- src/Concerns/Implementation.php | 2 +- src/Concerns/Relationships.php | 10 +++------- src/JsonApiResourceCollection.php | 8 ++++---- src/Link.php | 9 +++------ 4 files changed, 11 insertions(+), 18 deletions(-) diff --git a/src/Concerns/Implementation.php b/src/Concerns/Implementation.php index e7318cf..ab332e3 100644 --- a/src/Concerns/Implementation.php +++ b/src/Concerns/Implementation.php @@ -30,6 +30,6 @@ public static function resolveServerImplementationNormally(): void */ public static function serverImplementationResolver(): Closure { - return self::$serverImplementationResolver ?? fn () => new ServerImplementation('1.0'); + return self::$serverImplementationResolver ?? fn (): ServerImplementation => new ServerImplementation('1.0'); } } diff --git a/src/Concerns/Relationships.php b/src/Concerns/Relationships.php index 5b4bee4..3cb8354 100644 --- a/src/Concerns/Relationships.php +++ b/src/Concerns/Relationships.php @@ -33,9 +33,7 @@ trait Relationships */ public function withIncludePrefix(string $prefix): self { - $this->includePrefix = "{$this->includePrefix}{$prefix}."; - - return $this; + return tap($this, fn (JsonApiResource $resource): string => $resource->includePrefix = "{$this->includePrefix}{$prefix}."); } /** @@ -115,7 +113,7 @@ function (Closure $value, string $prefix) { /** * @param JsonApiResource|JsonApiResourceCollection|UnknownRelationship $resource */ - fn ($resource) => $resource instanceof PotentiallyMissing && $resource->isMissing() + fn ($resource): bool => $resource instanceof PotentiallyMissing && $resource->isMissing() )); } @@ -130,9 +128,7 @@ public function flush(): void /** * @param JsonApiResource|JsonApiResourceCollection|UnknownRelationship $resource */ - function ($resource): void { - $resource->flush(); - } + fn ($resource) => $resource->flush() ); } diff --git a/src/JsonApiResourceCollection.php b/src/JsonApiResourceCollection.php index 22160a1..f88d019 100644 --- a/src/JsonApiResourceCollection.php +++ b/src/JsonApiResourceCollection.php @@ -38,10 +38,10 @@ public function toResponse($request) */ public function withIncludePrefix(string $prefix): self { - /** @phpstan-ignore-next-line */ - $this->collection->each(fn (JsonApiResource $resource): JsonApiResource => $resource->withIncludePrefix($prefix)); - - return $this; + return tap($this, function (JsonApiResourceCollection $resource) use ($prefix): void { + /** @phpstan-ignore-next-line */ + $resource->collection->each(fn (JsonApiResource $resource): JsonApiResource => $resource->withIncludePrefix($prefix)); + }); } /** diff --git a/src/Link.php b/src/Link.php index adcfcd0..51d7ce1 100644 --- a/src/Link.php +++ b/src/Link.php @@ -19,12 +19,12 @@ class Link implements JsonSerializable public static function self(string $href, array $meta = []): self { - return tap(new self($href, $meta), fn (self $instance) => $instance->key = 'self'); + return tap(new self($href, $meta), fn (self $instance): string => $instance->key = 'self'); } public static function related(string $href, array $meta = []): self { - return tap(new self($href, $meta), fn (self $instance) => $instance->key = 'related'); + return tap(new self($href, $meta), fn (self $instance): string => $instance->key = 'related'); } private function __construct(string $href, array $meta = []) @@ -50,9 +50,6 @@ public function jsonSerialize(): array */ public function key(): string { - return [ - 'self' => 'self', - 'related' => 'related', - ][$this->key]; + return $this->key; } } From 993dc2b64b9a88d9b6c994eb26d678e3f58bbc33 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Sat, 18 Dec 2021 20:08:24 +1100 Subject: [PATCH 10/15] wip --- README.md | 18 ++++++++------- src/Concerns/Relationships.php | 2 +- src/JsonApiResource.php | 22 +++++++++++++++---- src/JsonApiResourceCollection.php | 4 ++-- src/JsonApiServerImplementation.php | 5 +---- src/Link.php | 5 +---- ...{Relationship.php => RelationshipLink.php} | 12 ++++------ src/ResourceIdentifier.php | 8 +------ src/Support/Cache.php | 2 +- src/Support/Fields.php | 2 +- src/Support/Includes.php | 2 +- src/Support/UnknownRelationship.php | 4 ++-- tests/Unit/JsonApiTest.php | 10 --------- 13 files changed, 43 insertions(+), 53 deletions(-) rename src/{Relationship.php => RelationshipLink.php} (57%) diff --git a/README.md b/README.md index 1706cb3..37ed9d7 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ # JSON:API Resource for Laravel -A lightweight JSON Resource for Laravel that helps you adhere to the JSON:API standards and also implements features such as sparse fieldsets and compound documents, whilst allowing you to extend the spec as needed for your project. +A lightweight JSON Resource for Laravel that helps you adhere to the JSON:API standard and also implements features such as sparse fieldsets and compound documents. -These docs are not designed to introduce you to the JSON:API spec and the associated concepts, instead you should [head over and read the spec](https:/jsonapi.org) if you are not familiar with it. +These docs are not designed to introduce you to the JSON:API spec and the associated concepts, instead you should [head over and read the spec](https:/jsonapi.org) if you are not familiar with it. The documentation that follows only contains information on _how_ to implement the specification via the package. # Version support @@ -139,6 +139,7 @@ class UserResource extends JsonApiResource { return [ Link::self(route('users.show', $this->resource)), + 'related' => 'https://example.com/related' ]; } } @@ -339,13 +340,8 @@ And a special (vegi) thanks to [Caneco](https://twitter.com/caneco) for the logo # Coming soon... -- [ ] Top level links - how would you modify this for a collection? - - [ ] decide how to handle top level keys for single and collections (static? should collections have to be extended to specify the values? or can there be static methods on the single resource for the collection?) -- [ ] Test assertions? - [ ] Handle loading relations on a already in memory object with Spatie Query builder (PR) -- [ ] Resource identifier links and meta as a new concept different to normal resource links and relationships. - [ ] Investigate collection count support -- [ ] Transducers for all the looping? - [ ] a contract that other classes can implement to support the JSON:API spec as relationships? Can we have it work at a top level as well? Would that even make sense? Maybe be providing a toResponse implementation? # To document @@ -358,6 +354,12 @@ And a special (vegi) thanks to [Caneco](https://twitter.com/caneco) for the logo - [ ] caching id and type - [ ] caching includes and fields - [ ] how it clears itself on toResponse - - [ ] asRelationship() - [ ] that the goal is to have a consistent output at all levels, hence the maximal dataset for empty values - [ ] Link object and meta + +# Not yet supported +- [ ] Top level links & meta - how would you modify this for a collection? Top level links need to merge with pagination links + - [ ] decide how to handle top level keys for single and collections (static? should collections have to be extended to specify the values? or can there be static methods on the single resource for the collection?) +- [ ] returning a resource as `null` as the Laravel resource does not support this. Is possible to support locally, but it might be unexpected. Perhaps a PR to Laravel is best? +- [ ] Responses that contain only resource identifiers (related) +- [ ] `400` when requesting relationships that are not present. diff --git a/src/Concerns/Relationships.php b/src/Concerns/Relationships.php index 3cb8354..c7fb600 100644 --- a/src/Concerns/Relationships.php +++ b/src/Concerns/Relationships.php @@ -85,7 +85,7 @@ private function requestedRelationshipsAsIdentifiers(Request $request): Collecti * @param JsonApiResource|JsonApiResourceCollection|UnknownRelationship $resource * @return mixed */ - fn ($resource) => $resource->asRelationship($request) + fn ($resource) => $resource->toResourceLink($request) ); } diff --git a/src/JsonApiResource.php b/src/JsonApiResource.php index 18005de..fb922ab 100644 --- a/src/JsonApiResource.php +++ b/src/JsonApiResource.php @@ -90,6 +90,8 @@ protected function toLinks(Request $request): array return [ // Link::self(route('users.show'), $this->resource), // Link::related(/** ... */), + // 'whatever' => 'Something' + // 'whateverElse' => new Link('whatever') ]; } @@ -126,19 +128,27 @@ protected function toType(Request $request): string * TODO: @see docs-link * @see https://jsonapi.org/format/#document-resource-object-linkage */ - public function asRelationship(Request $request): Relationship + public function toResourceLink(Request $request): RelationshipLink { - return new Relationship( + return new RelationshipLink( new ResourceIdentifier($this->resolveId($request), $this->resolveType($request)) ); } + /** + * @return mixed + */ + public function whenNull(Request $request, Closure $toArray) + { + return null; + } + /** * @param Request $request */ - public function toArray($request): array + public function toArray($request): ?array { - return [ + $toArray = fn () => [ 'id' => $this->resolveId($request), 'type' => $this->resolveType($request), 'attributes' => (object) $this->requestedAttributes($request)->all(), @@ -146,6 +156,10 @@ public function toArray($request): array 'meta' => (object) $this->toMeta($request), 'links' => (object) $this->resolveLinks($request), ]; + + return $this->resource === null + ? $this->whenNull($request, $toArray) + : $toArray(); } /** diff --git a/src/JsonApiResourceCollection.php b/src/JsonApiResourceCollection.php index f88d019..a6cd6fe 100644 --- a/src/JsonApiResourceCollection.php +++ b/src/JsonApiResourceCollection.php @@ -55,9 +55,9 @@ public function included(Request $request): Collection /** * @internal */ - public function asRelationship(Request $request): Collection + public function toResourceLink(Request $request): Collection { - return $this->collection->map(fn (JsonApiResource $resource): Relationship => $resource->asRelationship($request)); + return $this->collection->map(fn (JsonApiResource $resource): RelationshipLink => $resource->toResourceLink($request)); } /** diff --git a/src/JsonApiServerImplementation.php b/src/JsonApiServerImplementation.php index d1b89a7..03f1f84 100644 --- a/src/JsonApiServerImplementation.php +++ b/src/JsonApiServerImplementation.php @@ -6,7 +6,7 @@ use JsonSerializable; -class JsonApiServerImplementation implements JsonSerializable +final class JsonApiServerImplementation implements JsonSerializable { private string $version; @@ -19,9 +19,6 @@ public function __construct(string $version, array $meta = []) $this->meta = $meta; } - /** - * @internal - */ public function jsonSerialize(): array { return [ diff --git a/src/Link.php b/src/Link.php index 51d7ce1..11ab669 100644 --- a/src/Link.php +++ b/src/Link.php @@ -9,7 +9,7 @@ /** * @see https://jsonapi.org/format/#document-resource-object-links */ -class Link implements JsonSerializable +final class Link implements JsonSerializable { private string $href; @@ -34,9 +34,6 @@ private function __construct(string $href, array $meta = []) $this->meta = $meta; } - /** - * @internal - */ public function jsonSerialize(): array { return [ diff --git a/src/Relationship.php b/src/RelationshipLink.php similarity index 57% rename from src/Relationship.php rename to src/RelationshipLink.php index 7a224bc..b1a114e 100644 --- a/src/Relationship.php +++ b/src/RelationshipLink.php @@ -5,20 +5,16 @@ namespace TiMacDonald\JsonApi; use JsonSerializable; -use stdClass; -/** - * @see https://jsonapi.org/format/#document-resource-object-relationships - */ -class Relationship implements JsonSerializable +final class RelationshipLink implements JsonSerializable { - private ?ResourceIdentifier $data; + private ResourceIdentifier $data; private array $links; private array $meta; - public function __construct(?ResourceIdentifier $data = null, array $links = [], array $meta = []) + public function __construct(ResourceIdentifier $data, array $links = [], array $meta = []) { $this->data = $data; @@ -33,7 +29,7 @@ public function __construct(?ResourceIdentifier $data = null, array $links = [], public function jsonSerialize(): array { return [ - 'data' => $this->data ?? new stdClass(), + 'data' => $this->data, 'meta' => (object) $this->meta, 'links' => (object) $this->links, ]; diff --git a/src/ResourceIdentifier.php b/src/ResourceIdentifier.php index a7413a0..0bb11bd 100644 --- a/src/ResourceIdentifier.php +++ b/src/ResourceIdentifier.php @@ -6,10 +6,7 @@ use JsonSerializable; -/** - * @see https://jsonapi.org/format/#document-resource-identifier-objects - */ -class ResourceIdentifier implements JsonSerializable +final class ResourceIdentifier implements JsonSerializable { private string $id; @@ -26,9 +23,6 @@ public function __construct(string $id, string $type, array $meta = []) $this->meta = $meta; } - /** - * @internal - */ public function jsonSerialize(): array { return [ diff --git a/src/Support/Cache.php b/src/Support/Cache.php index 332e2b3..bc8b239 100644 --- a/src/Support/Cache.php +++ b/src/Support/Cache.php @@ -10,7 +10,7 @@ /** * @internal */ -class Cache +final class Cache { /** * @param JsonApiResource|JsonApiResourceCollection $resource diff --git a/src/Support/Fields.php b/src/Support/Fields.php index 9d5a653..3423900 100644 --- a/src/Support/Fields.php +++ b/src/Support/Fields.php @@ -13,7 +13,7 @@ /** * @internal */ -class Fields +final class Fields { private static ?Fields $instance; diff --git a/src/Support/Includes.php b/src/Support/Includes.php index 2ef25b6..a2616a8 100644 --- a/src/Support/Includes.php +++ b/src/Support/Includes.php @@ -15,7 +15,7 @@ /** * @internal */ -class Includes +final class Includes { private static ?Includes $instance; diff --git a/src/Support/UnknownRelationship.php b/src/Support/UnknownRelationship.php index 636637f..ff119ad 100644 --- a/src/Support/UnknownRelationship.php +++ b/src/Support/UnknownRelationship.php @@ -11,7 +11,7 @@ /** * @internal */ -class UnknownRelationship implements PotentiallyMissing +final class UnknownRelationship implements PotentiallyMissing { /** * @var mixed @@ -29,7 +29,7 @@ public function __construct($resource) /** * @return mixed */ - public function asRelationship(Request $request) + public function toResourceLink(Request $request) { return $this->resource; } diff --git a/tests/Unit/JsonApiTest.php b/tests/Unit/JsonApiTest.php index 493b67b..dbeb603 100644 --- a/tests/Unit/JsonApiTest.php +++ b/tests/Unit/JsonApiTest.php @@ -13,7 +13,6 @@ use TiMacDonald\JsonApi\JsonApiResource; use TiMacDonald\JsonApi\JsonApiServerImplementation; use TiMacDonald\JsonApi\Link; -use TiMacDonald\JsonApi\Relationship; use TiMacDonald\JsonApi\ResourceIdentifier; use TiMacDonald\JsonApi\Support\Fields; use TiMacDonald\JsonApi\Support\Includes; @@ -339,15 +338,6 @@ public function testItClearsTheHelperCachesAfterPreparingResponseForACollectionO $this->assertCount(0, Includes::getInstance()->cache()); } - public function testItCastsEmptyRelationshipsAttributesToObjects(): void - { - $relationship = new Relationship(null, [], []); - - $json = json_encode($relationship); - - self::assertSame('{"data":{},"meta":{},"links":{}}', $json); - } - public function testItCastsEmptyResourceIdentifierMetaToObject(): void { $relationship = new ResourceIdentifier('5', 'users'); From 40c68d5295897cd181b2f9d9f4ae2cb0d338868b Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Sat, 18 Dec 2021 20:20:12 +1100 Subject: [PATCH 11/15] wip --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 37ed9d7..e5a8d3f 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,9 @@ class UserResource extends JsonApiResource 'posts' => fn () => PostResource::collection($this->posts), 'subscription' => fn () => SubscriptionResource::make($this->subscription), 'profileImage' => fn () => optional($this->profileImage, fn (ProfileImage $profileImage) => ProfileImageResource::make($profileImage)), + // if the relationship has been loaded and is null, can we not just return the resource still and have a nice default? That way you never have to handle any of this + // optional noise? + // also is there a usecase for returning a resource linkage right from here and not a full resource? ]; } } From 9286280bc42fa5128a374b83f06c6dad0544c93a Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Tue, 21 Dec 2021 20:37:02 +1100 Subject: [PATCH 12/15] wip --- phpstan.neon | 4 +- phpunit.xml | 22 ++++---- src/Concerns/Caching.php | 86 +++++++++++++++++++++++++++++ src/Concerns/Identification.php | 32 ++--------- src/Concerns/Links.php | 7 ++- src/Concerns/Relationships.php | 44 --------------- src/Contracts/Flushable.php | 8 +++ src/JsonApiResource.php | 29 +++++----- src/JsonApiResourceCollection.php | 5 +- src/JsonApiServerImplementation.php | 10 ++++ src/Link.php | 19 ++++++- src/RelationshipLink.php | 13 ++++- src/ResourceIdentifier.php | 10 ++++ src/Support/Cache.php | 3 +- src/Support/Fields.php | 14 ++++- src/Support/Includes.php | 11 +++- src/Support/UnknownRelationship.php | 3 +- tests/Unit/JsonApiTest.php | 10 +++- 18 files changed, 218 insertions(+), 112 deletions(-) create mode 100644 src/Concerns/Caching.php create mode 100644 src/Contracts/Flushable.php diff --git a/phpstan.neon b/phpstan.neon index 9a23cb9..d59c469 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,10 +1,10 @@ parameters: - level: 8 + level: max paths: - src checkInternalClassCaseSensitivity: true checkTooWideReturnTypesInProtectedAndPublicMethods: true checkUninitializedProperties: true - checkMissingIterableValueType: false + checkMissingIterableValueType: true includes: - vendor-bin/linting/vendor/nunomaduro/larastan/extension.neon diff --git a/phpunit.xml b/phpunit.xml index 0b0d021..3173aab 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,23 +1,25 @@ - + bootstrap="vendor/autoload.php" + colors="true" +> tests/Unit - - + + tests/Feature - - src + + src + - - - + + diff --git a/src/Concerns/Caching.php b/src/Concerns/Caching.php new file mode 100644 index 0000000..7284a25 --- /dev/null +++ b/src/Concerns/Caching.php @@ -0,0 +1,86 @@ +idCache = null; + + $this->typeCache = null; + + if ($this->requestedRelationshipsCache !== null) { + $this->requestedRelationshipsCache->each( + /** + * @param JsonApiResource|JsonApiResourceCollection|UnknownRelationship $resource + */ + fn ($resource) => $resource->flush() + ); + } + + $this->requestedRelationshipsCache = null; + } + + /** + * @internal + * @infection-ignore-all + */ + private function rememberId(Closure $closure): string + { + return $this->idCache ??= $closure(); + } + + + /** + * @internal + * @infection-ignore-all + */ + private function rememberType(Closure $closure): string + { + return $this->typeCache ??= $closure(); + } + + /** + * @internal + * @infection-ignore-all + */ + private function rememberRequestRelationships(Closure $closure): Collection + { + return $this->requestedRelationshipsCache ??= $closure(); + } + + /** + * @internal + */ + public function requestedRelationshipsCache(): ?Collection + { + return $this->requestedRelationshipsCache; + } +} + diff --git a/src/Concerns/Identification.php b/src/Concerns/Identification.php index 8bd4312..20b4f58 100644 --- a/src/Concerns/Identification.php +++ b/src/Concerns/Identification.php @@ -25,16 +25,6 @@ trait Identification */ private static ?Closure $typeResolver; - /** - * @internal - */ - private ?string $idCache = null; - - /** - * @internal - */ - private ?string $typeCache = null; - /** * @internal */ @@ -75,24 +65,6 @@ private function resolveType(Request $request): string return $this->rememberType(fn (): string => $this->toType($request)); } - /** - * @internal - * @infection-ignore-all - */ - private function rememberType(Closure $closure): string - { - return $this->typeCache ??= $closure(); - } - - /** - * @internal - * @infection-ignore-all - */ - private function rememberId(Closure $closure): string - { - return $this->idCache ??= $closure(); - } - /** * @internal */ @@ -103,6 +75,10 @@ private static function idResolver(): Closure throw ResourceIdentificationException::attemptingToDetermineIdFor($resource); } + /** + * @see https://github.com/timacdonald/json-api#customising-the-resource-id + * @phpstan-ignore-next-line + */ return (string) $resource->getKey(); }; } diff --git a/src/Concerns/Links.php b/src/Concerns/Links.php index 6091dcb..a6aed76 100644 --- a/src/Concerns/Links.php +++ b/src/Concerns/Links.php @@ -15,18 +15,19 @@ trait Links { /** * @internal + * @return array */ private function resolveLinks(Request $request): array { return Collection::make($this->toLinks($request)) ->mapWithKeys( /** - * @param mixed $value - * @param int|string $key + * @param string|Link $value + * @param string|int $key */ fn ($value, $key): array => $value instanceof Link ? [$value->key() => $value] - : [$key => $value] + : [$key => new Link($value)] ) ->all(); } diff --git a/src/Concerns/Relationships.php b/src/Concerns/Relationships.php index c7fb600..1b75abc 100644 --- a/src/Concerns/Relationships.php +++ b/src/Concerns/Relationships.php @@ -23,11 +23,6 @@ trait Relationships */ private string $includePrefix = ''; - /** - * @internal - */ - private ?Collection $requestedRelationshipsCache = null; - /** * @internal */ @@ -117,45 +112,6 @@ function (Closure $value, string $prefix) { )); } - /** - * @internal - * @infection-ignore-all - */ - public function flush(): void - { - if ($this->requestedRelationshipsCache !== null) { - $this->requestedRelationshipsCache->each( - /** - * @param JsonApiResource|JsonApiResourceCollection|UnknownRelationship $resource - */ - fn ($resource) => $resource->flush() - ); - } - - $this->requestedRelationshipsCache = null; - - $this->idCache = null; - - $this->typeCache = null; - } - - /** - * @internal - * @infection-ignore-all - */ - private function rememberRequestRelationships(Closure $closure): Collection - { - return $this->requestedRelationshipsCache ??= $closure(); - } - - /** - * @internal - */ - public function requestedRelationshipsCache(): ?Collection - { - return $this->requestedRelationshipsCache; - } - /** * @internal */ diff --git a/src/Contracts/Flushable.php b/src/Contracts/Flushable.php new file mode 100644 index 0000000..8780d34 --- /dev/null +++ b/src/Contracts/Flushable.php @@ -0,0 +1,8 @@ + */ protected function toAttributes(Request $request): array { @@ -72,6 +77,7 @@ protected function toAttributes(Request $request): array /** * @see https://github.com/timacdonald/json-api#resource-relationships * @see https://jsonapi.org/format/#document-resource-object-relationships + * @return array */ protected function toRelationships(Request $request): array { @@ -84,6 +90,7 @@ protected function toRelationships(Request $request): array /** * @see https://github.com/timacdonald/json-api#resource-links * @see https://jsonapi.org/format/#document-resource-object-links + * @return array */ protected function toLinks(Request $request): array { @@ -98,6 +105,7 @@ protected function toLinks(Request $request): array /** * @see https://github.com/timacdonald/json-api#resource-meta * @see https://jsonapi.org/format/#document-meta + * @return array */ protected function toMeta(Request $request): array { @@ -135,20 +143,13 @@ public function toResourceLink(Request $request): RelationshipLink ); } - /** - * @return mixed - */ - public function whenNull(Request $request, Closure $toArray) - { - return null; - } - /** * @param Request $request + * @return array{id: string, type: string, attributes: stdClass, relationships: stdClass, meta: stdClass, links: stdClass} */ - public function toArray($request): ?array + public function toArray($request): array { - $toArray = fn () => [ + return [ 'id' => $this->resolveId($request), 'type' => $this->resolveType($request), 'attributes' => (object) $this->requestedAttributes($request)->all(), @@ -156,14 +157,11 @@ public function toArray($request): ?array 'meta' => (object) $this->toMeta($request), 'links' => (object) $this->resolveLinks($request), ]; - - return $this->resource === null - ? $this->whenNull($request, $toArray) - : $toArray(); } /** * @param Request $request + * @return array{included: Collection, jsonapi: JsonApiServerImplementation} */ public function with($request): array { @@ -176,6 +174,7 @@ public function with($request): array /** * @param mixed $resource + * @return JsonApiResourceCollection */ public static function collection($resource): JsonApiResourceCollection { diff --git a/src/JsonApiResourceCollection.php b/src/JsonApiResourceCollection.php index a6cd6fe..71d6b5c 100644 --- a/src/JsonApiResourceCollection.php +++ b/src/JsonApiResourceCollection.php @@ -7,12 +7,14 @@ use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\AnonymousResourceCollection; use Illuminate\Support\Collection; +use TiMacDonald\JsonApi\Contracts\Flushable; use TiMacDonald\JsonApi\Support\Cache; -class JsonApiResourceCollection extends AnonymousResourceCollection +class JsonApiResourceCollection extends AnonymousResourceCollection implements Flushable { /** * @param Request $request + * @return array{included: Collection, jsonapi: JsonApiResource} */ public function with($request): array { @@ -35,6 +37,7 @@ public function toResponse($request) /** * @internal + * @return JsonApiResourceCollection */ public function withIncludePrefix(string $prefix): self { diff --git a/src/JsonApiServerImplementation.php b/src/JsonApiServerImplementation.php index 03f1f84..7d90b3d 100644 --- a/src/JsonApiServerImplementation.php +++ b/src/JsonApiServerImplementation.php @@ -5,13 +5,20 @@ namespace TiMacDonald\JsonApi; use JsonSerializable; +use stdClass; final class JsonApiServerImplementation implements JsonSerializable { private string $version; + /** + * @var array + */ private array $meta; + /** + * @param array $meta + */ public function __construct(string $version, array $meta = []) { $this->version = $version; @@ -19,6 +26,9 @@ public function __construct(string $version, array $meta = []) $this->meta = $meta; } + /** + * @return array{version: string, meta: stdClass} + */ public function jsonSerialize(): array { return [ diff --git a/src/Link.php b/src/Link.php index 11ab669..807b65f 100644 --- a/src/Link.php +++ b/src/Link.php @@ -5,6 +5,7 @@ namespace TiMacDonald\JsonApi; use JsonSerializable; +use stdClass; /** * @see https://jsonapi.org/format/#document-resource-object-links @@ -13,27 +14,43 @@ final class Link implements JsonSerializable { private string $href; + /** + * @var array + */ private array $meta; private string $key = 'unknown'; + /** + * @param array $meta + */ public static function self(string $href, array $meta = []): self { return tap(new self($href, $meta), fn (self $instance): string => $instance->key = 'self'); } + /** + * @param array $meta + */ public static function related(string $href, array $meta = []): self { return tap(new self($href, $meta), fn (self $instance): string => $instance->key = 'related'); } - private function __construct(string $href, array $meta = []) + /** + * @internal + * @param array $meta + */ + public function __construct(string $href, array $meta = []) { $this->href = $href; $this->meta = $meta; } + /** + * @return array{href: string, meta: stdClass} + */ public function jsonSerialize(): array { return [ diff --git a/src/RelationshipLink.php b/src/RelationshipLink.php index b1a114e..435d802 100644 --- a/src/RelationshipLink.php +++ b/src/RelationshipLink.php @@ -5,15 +5,26 @@ namespace TiMacDonald\JsonApi; use JsonSerializable; +use stdClass; final class RelationshipLink implements JsonSerializable { private ResourceIdentifier $data; + /** + * @var array + */ private array $links; + /** + * @var array + */ private array $meta; + /** + * @param array $links + * @param array $meta + */ public function __construct(ResourceIdentifier $data, array $links = [], array $meta = []) { $this->data = $data; @@ -24,7 +35,7 @@ public function __construct(ResourceIdentifier $data, array $links = [], array $ } /** - * @internal + * @return array{data: ResourceIdentifier, meta: stdClass, links: stdClass} */ public function jsonSerialize(): array { diff --git a/src/ResourceIdentifier.php b/src/ResourceIdentifier.php index 0bb11bd..73edc0f 100644 --- a/src/ResourceIdentifier.php +++ b/src/ResourceIdentifier.php @@ -5,6 +5,7 @@ namespace TiMacDonald\JsonApi; use JsonSerializable; +use stdClass; final class ResourceIdentifier implements JsonSerializable { @@ -12,8 +13,14 @@ final class ResourceIdentifier implements JsonSerializable private string $type; + /** + * @var array + */ private array $meta; + /** + * @param array $meta + */ public function __construct(string $id, string $type, array $meta = []) { $this->id = $id; @@ -23,6 +30,9 @@ public function __construct(string $id, string $type, array $meta = []) $this->meta = $meta; } + /** + * @return array{id: string, type: string, meta: stdClass} + */ public function jsonSerialize(): array { return [ diff --git a/src/Support/Cache.php b/src/Support/Cache.php index bc8b239..454b93c 100644 --- a/src/Support/Cache.php +++ b/src/Support/Cache.php @@ -4,6 +4,7 @@ namespace TiMacDonald\JsonApi\Support; +use TiMacDonald\JsonApi\Contracts\Flushable; use TiMacDonald\JsonApi\JsonApiResource; use TiMacDonald\JsonApi\JsonApiResourceCollection; @@ -13,7 +14,7 @@ final class Cache { /** - * @param JsonApiResource|JsonApiResourceCollection $resource + * @param Flushable $resource */ public static function flush($resource): void { diff --git a/src/Support/Fields.php b/src/Support/Fields.php index 3423900..ab63364 100644 --- a/src/Support/Fields.php +++ b/src/Support/Fields.php @@ -6,6 +6,8 @@ use Closure; use Illuminate\Http\Request; +use TiMacDonald\JsonApi\Contracts\Flushable; + use function array_key_exists; use function explode; use function is_string; @@ -13,10 +15,13 @@ /** * @internal */ -final class Fields +final class Fields implements Flushable { private static ?Fields $instance; + /** + * @var array|null> + */ private array $cache = []; private function __construct() @@ -29,6 +34,9 @@ public static function getInstance(): self return self::$instance ??= new self(); } + /** + * @return array + */ public function parse(Request $request, string $resourceType, bool $minimalAttributes): ?array { return $this->rememberResourceType("type:{$resourceType};minimal:{$minimalAttributes};", function () use ($request, $resourceType, $minimalAttributes): ?array { @@ -56,6 +64,7 @@ public function parse(Request $request, string $resourceType, bool $minimalAttri /** * @infection-ignore-all + * @return array */ private function rememberResourceType(string $resourceType, Closure $callback): ?array { @@ -67,6 +76,9 @@ public function flush(): void $this->cache = []; } + /** + * @return array|null> + */ public function cache(): array { return $this->cache; diff --git a/src/Support/Includes.php b/src/Support/Includes.php index a2616a8..1544945 100644 --- a/src/Support/Includes.php +++ b/src/Support/Includes.php @@ -8,6 +8,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Collection; use Illuminate\Support\Str; +use TiMacDonald\JsonApi\Contracts\Flushable; use function explode; use function is_array; @@ -15,10 +16,13 @@ /** * @internal */ -final class Includes +final class Includes implements Flushable { private static ?Includes $instance; + /** + * @var array + */ private array $cache = []; private function __construct() @@ -42,7 +46,7 @@ public function parse(Request $request, string $prefix): Collection ->when($prefix !== '', function (Collection $includes) use ($prefix): Collection { return $includes->filter(fn (string $include): bool => Str::startsWith($include, $prefix)); }) - ->map(fn (string $include): string => Str::before(Str::after($include, $prefix), '.')) + ->map(fn ($include): string => Str::before(Str::after($include, $prefix), '.')) ->uniqueStrict() ->filter(fn (string $include): bool => $include !== ''); }); @@ -61,6 +65,9 @@ public function flush(): void $this->cache = []; } + /** + * @return array + */ public function cache(): array { return $this->cache; diff --git a/src/Support/UnknownRelationship.php b/src/Support/UnknownRelationship.php index ff119ad..7114e30 100644 --- a/src/Support/UnknownRelationship.php +++ b/src/Support/UnknownRelationship.php @@ -7,11 +7,12 @@ use Illuminate\Http\Request; use Illuminate\Http\Resources\PotentiallyMissing; use Illuminate\Support\Collection; +use TiMacDonald\JsonApi\Contracts\Flushable; /** * @internal */ -final class UnknownRelationship implements PotentiallyMissing +final class UnknownRelationship implements PotentiallyMissing, Flushable { /** * @var mixed diff --git a/tests/Unit/JsonApiTest.php b/tests/Unit/JsonApiTest.php index dbeb603..41ab9af 100644 --- a/tests/Unit/JsonApiTest.php +++ b/tests/Unit/JsonApiTest.php @@ -162,7 +162,10 @@ protected function toLinks(Request $request): array 'relationships' => [], 'meta' => [], 'links' => [ - 'links-key' => 'links-value', + 'links-key' => [ + 'href' => 'links-value', + 'meta' => [], + ] ], ], 'included' => [], @@ -205,7 +208,10 @@ protected function toLinks(Request $request): array 'href' => 'https://example.test/related', 'meta' => [], ], - 'home' => 'https://example.test', + 'home' => [ + 'href' => 'https://example.test', + 'meta' => [] + ], ], ], 'included' => [], From 72dfc8b9a6d62fba617d9bde48f2bb878210e84e Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Tue, 21 Dec 2021 20:39:37 +1100 Subject: [PATCH 13/15] wip --- src/Concerns/Caching.php | 3 ++- src/Contracts/Flushable.php | 2 ++ src/JsonApiResource.php | 2 +- src/Support/Cache.php | 2 -- tests/Unit/JsonApiTest.php | 4 ++-- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Concerns/Caching.php b/src/Concerns/Caching.php index 7284a25..f4ee857 100644 --- a/src/Concerns/Caching.php +++ b/src/Concerns/Caching.php @@ -1,5 +1,7 @@ requestedRelationshipsCache; } } - diff --git a/src/Contracts/Flushable.php b/src/Contracts/Flushable.php index 8780d34..64bfce1 100644 --- a/src/Contracts/Flushable.php +++ b/src/Contracts/Flushable.php @@ -1,5 +1,7 @@ + * @return array */ protected function toRelationships(Request $request): array { diff --git a/src/Support/Cache.php b/src/Support/Cache.php index 454b93c..d3f427c 100644 --- a/src/Support/Cache.php +++ b/src/Support/Cache.php @@ -5,8 +5,6 @@ namespace TiMacDonald\JsonApi\Support; use TiMacDonald\JsonApi\Contracts\Flushable; -use TiMacDonald\JsonApi\JsonApiResource; -use TiMacDonald\JsonApi\JsonApiResourceCollection; /** * @internal diff --git a/tests/Unit/JsonApiTest.php b/tests/Unit/JsonApiTest.php index 41ab9af..7b969a4 100644 --- a/tests/Unit/JsonApiTest.php +++ b/tests/Unit/JsonApiTest.php @@ -165,7 +165,7 @@ protected function toLinks(Request $request): array 'links-key' => [ 'href' => 'links-value', 'meta' => [], - ] + ], ], ], 'included' => [], @@ -210,7 +210,7 @@ protected function toLinks(Request $request): array ], 'home' => [ 'href' => 'https://example.test', - 'meta' => [] + 'meta' => [], ], ], ], From 1bea7e2df2eb7f20cee55ea6cba6e7b8858be2f3 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Tue, 21 Dec 2021 20:46:27 +1100 Subject: [PATCH 14/15] wip --- tests/Unit/JsonApiTest.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/Unit/JsonApiTest.php b/tests/Unit/JsonApiTest.php index 7b969a4..2334cb3 100644 --- a/tests/Unit/JsonApiTest.php +++ b/tests/Unit/JsonApiTest.php @@ -13,6 +13,7 @@ use TiMacDonald\JsonApi\JsonApiResource; use TiMacDonald\JsonApi\JsonApiServerImplementation; use TiMacDonald\JsonApi\Link; +use TiMacDonald\JsonApi\RelationshipLink; use TiMacDonald\JsonApi\ResourceIdentifier; use TiMacDonald\JsonApi\Support\Fields; use TiMacDonald\JsonApi\Support\Includes; @@ -407,4 +408,15 @@ public function testItCanSpecifyAnImplementation(): void BasicJsonApiResource::resolveServerImplementationNormally(); } + + public function testItCastsEmptyRelationshipLinkMetaToJsonObject() + { + $resourceLink = new RelationshipLink( + new ResourceIdentifier('expected-id', 'expected-type') + ); + + $json = json_encode($resourceLink); + + self::assertSame('{"data":{"id":"expected-id","type":"expected-type","meta":{}},"meta":{},"links":{}}', $json); + } } From 0e4fad5a5088d1917d20f8cd66974c408f844486 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Tue, 21 Dec 2021 20:49:12 +1100 Subject: [PATCH 15/15] wip --- src/Support/Includes.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Support/Includes.php b/src/Support/Includes.php index 1544945..95b4a0a 100644 --- a/src/Support/Includes.php +++ b/src/Support/Includes.php @@ -42,11 +42,13 @@ public function parse(Request $request, string $prefix): Collection abort_if(is_array($includes), 400, 'The include parameter must be a comma seperated list of relationship paths.'); - return Collection::make(explode(',', $includes)) + /** @var Collection */ + $includes = Collection::make(explode(',', $includes)) ->when($prefix !== '', function (Collection $includes) use ($prefix): Collection { return $includes->filter(fn (string $include): bool => Str::startsWith($include, $prefix)); - }) - ->map(fn ($include): string => Str::before(Str::after($include, $prefix), '.')) + }); + + return $includes->map(fn ($include): string => Str::before(Str::after($include, $prefix), '.')) ->uniqueStrict() ->filter(fn (string $include): bool => $include !== ''); });