From 88e5ee2999dab98c76802b8bfcb6f81eb579ef70 Mon Sep 17 00:00:00 2001 From: Lynh Date: Sat, 9 Mar 2024 22:24:20 +0700 Subject: [PATCH] Refactor --- README.md | 55 ++++++++++++++++++++------------ composer.json | 6 ++-- src/DTO/ErrorResponse.php | 21 ++++++++++++ src/HasPagination.php | 28 ++++++++++++++++ src/JsonPlaceholder.php | 29 +++-------------- src/Post/FindPostRequest.php | 3 +- src/Post/GetPostsRequest.php | 14 +++++++- src/Post/PostResource.php | 37 ++++++++++++++++----- src/ResourceBuilder.php | 46 ++++++++++++++++++++++++++ src/User/FindUserRequest.php | 3 +- src/User/GetUserPostsRequest.php | 14 +++++++- src/User/GetUsersRequest.php | 14 +++++++- src/User/UserResource.php | 39 ++++++++++++++-------- src/ValinorDecoder.php | 3 +- tests/PostTest.php | 12 +++++-- tests/UserTest.php | 10 ++++-- 16 files changed, 254 insertions(+), 80 deletions(-) create mode 100644 src/DTO/ErrorResponse.php create mode 100644 src/HasPagination.php create mode 100644 src/ResourceBuilder.php diff --git a/README.md b/README.md index 49e3e2b..a2599bd 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# This is my package jsonplaceholder +# JsonPlaceholder PHP SDK example [![Latest Version on Packagist][ico-version]][link-packagist] [![Github Actions][ico-gh-actions]][link-gh-actions] @@ -7,7 +7,7 @@ [![Total Downloads][ico-downloads]][link-downloads] [![Software License][ico-license]](LICENSE.md) -This is where your description should go. Try and limit it to a paragraph or two. Consider adding a small example. +This repository serves as a demonstration of how you can build your SDK/integration with [JsonPlaceholder](https://jsonplaceholder.typicode.com/) service using [Fansipan](https://github.com/phanxipang/fansipan) library. ## Installation @@ -19,9 +19,30 @@ composer require jenky/jsonplaceholder ## Usage +Create new SDK instance + +```php +$sdk = new Jenky\JsonPlaceholder(); +``` + +Get list of users + +```php +// GET https://jsonplaceholder.typicode.com/users +$sdk->users()->get(); + +// GET https://jsonplaceholder.typicode.com/users?_limit=5 +$sdk->users()->get(limit: 5); + +// GET https://jsonplaceholder.typicode.com/users?_page=2 +$sdk->users()->get(page: 2); +``` + +Get an user by ID + ```php -$jsonplaceholder = new Jenky\JsonPlaceholder(); -echo $jsonplaceholder->echoPhrase('Hello, Jenky!'); +// GET https://jsonplaceholder.typicode.com/users/1 +$sdk->users()->id(1)->find(); ``` ## Testing @@ -40,7 +61,7 @@ Please see [CONTRIBUTING](CONTRIBUTING.md) and [CODE_OF_CONDUCT](CODE_OF_CONDUCT ## Security -If you discover any security related issues, please email jenky.w0w@gmail.com instead of using the issue tracker. +If you discover any security related issues, please email contact@lynh.me instead of using the issue tracker. ## Credits @@ -51,20 +72,14 @@ If you discover any security related issues, please email jenky.w0w@gmail.com in The MIT License (MIT). Please see [License File](LICENSE.md) for more information. -[ico-version]: https://img.shields.io/packagist/v/jenky/JsonPlaceholder.svg?style=for-the-badge +[ico-version]: https://img.shields.io/packagist/v/jenky/jsonplaceholder.svg?style=for-the-badge [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=for-the-badge -[ico-travis]: https://img.shields.io/travis/jenky/JsonPlaceholder/master.svg?style=for-the-badge -[ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/jenky/JsonPlaceholder.svg?style=for-the-badge -[ico-code-quality]: https://img.shields.io/scrutinizer/g/jenky/JsonPlaceholder.svg?style=for-the-badge -[ico-gh-actions]: https://img.shields.io/github/actions/workflow/status/jenky/JsonPlaceholder/testing.yml?branch=main&label=actions&logo=github&style=for-the-badge -[ico-codecov]: https://img.shields.io/codecov/c/github/jenky/JsonPlaceholder?logo=codecov&style=for-the-badge -[ico-downloads]: https://img.shields.io/packagist/dt/jenky/JsonPlaceholder.svg?style=for-the-badge - -[link-packagist]: https://packagist.org/packages/jenky/JsonPlaceholder -[link-travis]: https://travis-ci.org/jenky/JsonPlaceholder -[link-scrutinizer]: https://scrutinizer-ci.com/g/jenky/JsonPlaceholder/code-structure -[link-code-quality]: https://scrutinizer-ci.com/g/jenky/JsonPlaceholder -[link-gh-actions]: https://github.com/jenky/jenky/JsonPlaceholder -[link-codecov]: https://codecov.io/gh/jenky/JsonPlaceholder -[link-downloads]: https://packagist.org/packages/jenky/JsonPlaceholder +[ico-gh-actions]: https://img.shields.io/github/actions/workflow/status/jenky/jsonplaceholder/testing.yml?branch=main&label=actions&logo=github&style=for-the-badge +[ico-codecov]: https://img.shields.io/codecov/c/github/jenky/jsonplaceholder?logo=codecov&style=for-the-badge +[ico-downloads]: https://img.shields.io/packagist/dt/jenky/jsonplaceholder.svg?style=for-the-badge + +[link-packagist]: https://packagist.org/packages/jenky/jsonplaceholder +[link-gh-actions]: https://github.com/jenky/jenky/jsonplaceholder +[link-codecov]: https://codecov.io/gh/jenky/jsonplaceholder +[link-downloads]: https://packagist.org/packages/jenky/jsonplaceholder diff --git a/composer.json b/composer.json index 7f59788..3fc55c8 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "jenky/jsonplaceholder", - "description": "Example of Atlas using JSON Placeholder", + "description": "JSON Placeholder SDK", "keywords": [ "atlas", "jsonplaceholder", @@ -17,8 +17,8 @@ ], "require": { "php": "^8.1", - "cuyz/valinor": "^1.4", - "fansipan/fansipan": "^0.8", + "cuyz/valinor": "^1.9", + "fansipan/fansipan": "^1.0", "guzzlehttp/guzzle": "^7.7", "ramsey/collection": "^2.0" }, diff --git a/src/DTO/ErrorResponse.php b/src/DTO/ErrorResponse.php new file mode 100644 index 0000000..98a8783 --- /dev/null +++ b/src/DTO/ErrorResponse.php @@ -0,0 +1,21 @@ +getStatusCode(), $response->getReasonPhrase()); + } +} diff --git a/src/HasPagination.php b/src/HasPagination.php new file mode 100644 index 0000000..05bd4b4 --- /dev/null +++ b/src/HasPagination.php @@ -0,0 +1,28 @@ +page = $limit; + + return $clone; + } + + public function page(int $page): self + { + $clone = clone $this; + $clone->page = $page; + + return $clone; + } +} diff --git a/src/JsonPlaceholder.php b/src/JsonPlaceholder.php index 8dd5bd5..9b099e4 100644 --- a/src/JsonPlaceholder.php +++ b/src/JsonPlaceholder.php @@ -6,11 +6,7 @@ use Fansipan\Contracts\ConnectorInterface; use Fansipan\Traits\ConnectorTrait; -use Jenky\JsonPlaceholder\DTO\PostCollection; -use Jenky\JsonPlaceholder\DTO\UserCollection; -use Jenky\JsonPlaceholder\Post\GetPostsRequest; use Jenky\JsonPlaceholder\Post\PostResource; -use Jenky\JsonPlaceholder\User\GetUsersRequest; use Jenky\JsonPlaceholder\User\UserResource; final class JsonPlaceholder implements ConnectorInterface @@ -25,30 +21,13 @@ public static function baseUri(): ?string /** * Get list of users. */ - public function users(): UserCollection + public function users(): UserResource { - return $this->send(new GetUsersRequest()) - ->throw() - ->object(); + return new UserResource($this); } - public function user(int $id): UserResource + public function posts(): PostResource { - return new UserResource($this, $id); - } - - /** - * Get list of posts. - */ - public function posts(): PostCollection - { - return $this->send(new GetPostsRequest()) - ->throw() - ->object(); - } - - public function post(int $id): PostResource - { - return new PostResource($this, $id); + return new PostResource($this); } } diff --git a/src/Post/FindPostRequest.php b/src/Post/FindPostRequest.php index 5cdc059..d8b2ece 100644 --- a/src/Post/FindPostRequest.php +++ b/src/Post/FindPostRequest.php @@ -6,11 +6,12 @@ use Fansipan\Contracts\DecoderInterface; use Fansipan\Request; +use Jenky\JsonPlaceholder\DTO\ErrorResponse; use Jenky\JsonPlaceholder\DTO\Post; use Jenky\JsonPlaceholder\ValinorDecoder; /** - * @extends Request + * @extends Request */ final class FindPostRequest extends Request { diff --git a/src/Post/GetPostsRequest.php b/src/Post/GetPostsRequest.php index 86089b2..8cfd2a2 100644 --- a/src/Post/GetPostsRequest.php +++ b/src/Post/GetPostsRequest.php @@ -6,19 +6,31 @@ use Fansipan\Contracts\DecoderInterface; use Fansipan\Request; +use Jenky\JsonPlaceholder\DTO\ErrorResponse; use Jenky\JsonPlaceholder\DTO\PostCollection; +use Jenky\JsonPlaceholder\HasPagination; use Jenky\JsonPlaceholder\ValinorDecoder; /** - * @extends Request + * @extends Request */ final class GetPostsRequest extends Request { + use HasPagination; + public function endpoint(): string { return '/posts'; } + protected function defaultQuery(): array + { + return \array_filter([ + '_page' => $this->page, + '_limit' => $this->limit, + ]); + } + public function decoder(): DecoderInterface { return new ValinorDecoder(PostCollection::class); diff --git a/src/Post/PostResource.php b/src/Post/PostResource.php index 5f6428a..c97f440 100644 --- a/src/Post/PostResource.php +++ b/src/Post/PostResource.php @@ -4,24 +4,45 @@ namespace Jenky\JsonPlaceholder\Post; +use Jenky\JsonPlaceholder\DTO\ErrorResponse; use Jenky\JsonPlaceholder\DTO\Post; +use Jenky\JsonPlaceholder\DTO\PostCollection; use Jenky\JsonPlaceholder\JsonPlaceholder; +use Jenky\JsonPlaceholder\ResourceBuilder; +use Jenky\JsonPlaceholder\User\GetUserPostsRequest; +use Jenky\JsonPlaceholder\User\UserResource; -final class PostResource +/** + * @extends ResourceBuilder + */ +final class PostResource extends ResourceBuilder { - public function __construct( - private JsonPlaceholder $connector, - private int $id - ) { + /** + * Get list of posts for user. + */ + public function get(?int $page = null, ?int $limit = null): PostCollection|ErrorResponse + { + $userId = $this->refs[UserResource::class] ?? null; + $request = $userId ? new GetUserPostsRequest($userId) : new GetPostsRequest(); + + if ($page) { + $request = $request->page($page); + } + + if ($limit) { + $request = $request->page($limit); + } + + return $this->connector->send($request) + ->object(); } /** * Get a single post. */ - public function find(): Post + public function find(): Post|ErrorResponse { - return $this->connector->send(new FindPostRequest($this->id)) - ->throw() + return $this->connector->send(new FindPostRequest((int) $this->id)) ->object(); } } diff --git a/src/ResourceBuilder.php b/src/ResourceBuilder.php new file mode 100644 index 0000000..f493ab3 --- /dev/null +++ b/src/ResourceBuilder.php @@ -0,0 +1,46 @@ +id = $id; + $clone->refs[static::class] = $id; + + return $clone; + } + + /** + * @template TResource of ResourceBuilder + * + * @param class-string $builder + * @return TResource + */ + protected function forward(string $builder): ResourceBuilder + { + \assert(\is_subclass_of($builder, ResourceBuilder::class, true)); + + return new $builder($this->connector, $this->refs); + } +} diff --git a/src/User/FindUserRequest.php b/src/User/FindUserRequest.php index dc610e7..8e49c8f 100644 --- a/src/User/FindUserRequest.php +++ b/src/User/FindUserRequest.php @@ -6,11 +6,12 @@ use Fansipan\Contracts\DecoderInterface; use Fansipan\Request; +use Jenky\JsonPlaceholder\DTO\ErrorResponse; use Jenky\JsonPlaceholder\DTO\User; use Jenky\JsonPlaceholder\ValinorDecoder; /** - * @extends Request + * @extends Request */ final class FindUserRequest extends Request { diff --git a/src/User/GetUserPostsRequest.php b/src/User/GetUserPostsRequest.php index 2e951b0..02da81e 100644 --- a/src/User/GetUserPostsRequest.php +++ b/src/User/GetUserPostsRequest.php @@ -6,14 +6,18 @@ use Fansipan\Contracts\DecoderInterface; use Fansipan\Request; +use Jenky\JsonPlaceholder\DTO\ErrorResponse; use Jenky\JsonPlaceholder\DTO\PostCollection; +use Jenky\JsonPlaceholder\HasPagination; use Jenky\JsonPlaceholder\ValinorDecoder; /** - * @extends Request + * @extends Request */ final class GetUserPostsRequest extends Request { + use HasPagination; + public function __construct(private int $id) { } @@ -23,6 +27,14 @@ public function endpoint(): string return '/users/'.$this->id.'/posts'; } + protected function defaultQuery(): array + { + return \array_filter([ + '_page' => $this->page, + '_limit' => $this->limit, + ]); + } + public function decoder(): DecoderInterface { return new ValinorDecoder(PostCollection::class); diff --git a/src/User/GetUsersRequest.php b/src/User/GetUsersRequest.php index f180be0..797920a 100644 --- a/src/User/GetUsersRequest.php +++ b/src/User/GetUsersRequest.php @@ -6,19 +6,31 @@ use Fansipan\Contracts\DecoderInterface; use Fansipan\Request; +use Jenky\JsonPlaceholder\DTO\ErrorResponse; use Jenky\JsonPlaceholder\DTO\UserCollection; +use Jenky\JsonPlaceholder\HasPagination; use Jenky\JsonPlaceholder\ValinorDecoder; /** - * @extends Request + * @extends Request */ final class GetUsersRequest extends Request { + use HasPagination; + public function endpoint(): string { return '/users'; } + protected function defaultQuery(): array + { + return \array_filter([ + '_page' => $this->page, + '_limit' => $this->limit, + ]); + } + public function decoder(): DecoderInterface { return new ValinorDecoder(UserCollection::class); diff --git a/src/User/UserResource.php b/src/User/UserResource.php index 497935a..5a6af0b 100644 --- a/src/User/UserResource.php +++ b/src/User/UserResource.php @@ -4,35 +4,48 @@ namespace Jenky\JsonPlaceholder\User; -use Jenky\JsonPlaceholder\DTO\PostCollection; +use Jenky\JsonPlaceholder\DTO\ErrorResponse; use Jenky\JsonPlaceholder\DTO\User; +use Jenky\JsonPlaceholder\DTO\UserCollection; use Jenky\JsonPlaceholder\JsonPlaceholder; +use Jenky\JsonPlaceholder\Post\PostResource; +use Jenky\JsonPlaceholder\ResourceBuilder; -final class UserResource +/** + * @extends ResourceBuilder + */ +final class UserResource extends ResourceBuilder { - public function __construct( - private JsonPlaceholder $connector, - private int $id - ) { + public function get(?int $page = null, ?int $limit = null): UserCollection|ErrorResponse + { + $request = new GetUsersRequest(); + + if ($page) { + $request = $request->page($page); + } + + if ($limit) { + $request = $request->page($limit); + } + + return $this->connector->send($request) + ->object(); } /** * Get a single user. */ - public function find(): User + public function find(): User|ErrorResponse { - return $this->connector->send(new FindUserRequest($this->id)) - ->throw() + return $this->connector->send(new FindUserRequest((int) $this->id)) ->object(); } /** * Get list of posts for user. */ - public function posts(): PostCollection + public function posts(): PostResource { - return $this->connector->send(new GetUserPostsRequest($this->id)) - ->throw() - ->object(); + return $this->forward(PostResource::class); } } diff --git a/src/ValinorDecoder.php b/src/ValinorDecoder.php index ab6844d..1c88482 100644 --- a/src/ValinorDecoder.php +++ b/src/ValinorDecoder.php @@ -9,6 +9,7 @@ use Fansipan\Contracts\DecoderInterface; use Fansipan\Contracts\MapperInterface; use Fansipan\Decoder\ChainDecoder; +use Jenky\JsonPlaceholder\DTO\ErrorResponse; use Psr\Http\Message\ResponseInterface; /** @@ -43,7 +44,7 @@ public function map(ResponseInterface $response): ?object if ($status >= 200 && $status < 300) { return $this->mapper->map($this->signature, $this->decode($response)); } else { - return null; + return ErrorResponse::fromResponse($response); // @phpstan-ignore-line } } diff --git a/tests/PostTest.php b/tests/PostTest.php index b8b8c19..6c88d35 100644 --- a/tests/PostTest.php +++ b/tests/PostTest.php @@ -13,16 +13,24 @@ public function test_get_list_posts(): void { $posts = $this->sdk->withClient($this->createMockClient( __DIR__.'/fixtures/post/posts.json' - ))->posts(); + ))->posts()->get(); $this->assertInstanceOf(PostCollection::class, $posts); $this->assertCount(100, $posts); $this->assertSame('sunt aut facere repellat provident occaecati excepturi optio reprehenderit', $posts[0]->title); } + public function test_get_list_of_post_with_pagination(): void + { + $posts = $this->sdk->posts()->get(2, 10); + + $this->assertInstanceOf(PostCollection::class, $posts); + $this->assertCount(10, $posts); + } + public function test_find_post_by_id(): void { - $post = $this->sdk->post($id = rand(1, 10))->find(); + $post = $this->sdk->posts()->id($id = rand(1, 10))->find(); $this->assertInstanceOf(Post::class, $post); $this->assertSame($id, $post->id); diff --git a/tests/UserTest.php b/tests/UserTest.php index 319b186..1c2ff7e 100644 --- a/tests/UserTest.php +++ b/tests/UserTest.php @@ -14,7 +14,7 @@ public function test_get_list_users(): void { $users = $this->sdk->withClient($this->createMockClient( __DIR__.'/fixtures/user/users.json' - ))->users(); + ))->users()->get(); $this->assertInstanceOf(UserCollection::class, $users); $this->assertCount(10, $users); @@ -23,7 +23,7 @@ public function test_get_list_users(): void public function test_find_user_by_id(): void { - $user = $this->sdk->user($id = rand(1, 10))->find(); + $user = $this->sdk->users()->id($id = rand(1, 10))->find(); $this->assertInstanceOf(User::class, $user); $this->assertSame($id, $user->id); @@ -33,7 +33,11 @@ public function test_get_user_posts_by_user_id(): void { $posts = $this->sdk->withClient( $this->createMockClient(__DIR__.'/fixtures/user/posts.json') - )->user(1)->posts(); + ) + ->users() + ->id(1) + ->posts() + ->get(); $this->assertInstanceOf(PostCollection::class, $posts); $this->assertCount(10, $posts);