From 56b273054c8de4c4931950c943aab3fa69b94600 Mon Sep 17 00:00:00 2001 From: soulaimaneyh Date: Thu, 9 Nov 2023 20:06:37 +0100 Subject: [PATCH] =?UTF-8?q?init=20=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/tests.yml | 30 +++++ .gitignore | 6 + LICENSE | 21 +++ README.md | 127 ++++++++++++++++++ assets/api-responser.svg | 1 + composer.json | 60 +++++++++ config/api-responser.php | 13 ++ src/Facades/ApiResponser.php | 20 +++ src/Interfaces/ApiRepositoryInterface.php | 36 +++++ src/Providers/ApiResponserServiceProvider.php | 37 +++++ src/Repositories/ApiRepository.php | 65 +++++++++ src/Traits/ApiResponser.php | 88 ++++++++++++ tests/BaseTest.php | 42 ++++++ tests/Feature/.gitkeep | 0 tests/Feature/ApiResponserShowAllTest.php | 34 +++++ tests/Feature/ApiResponserShowOneTest.php | 32 +++++ .../Feature/Traits/ApiResponserTestTrait.php | 74 ++++++++++ tests/Models/Post.php | 16 +++ tests/Unit/.gitkeep | 0 19 files changed, 702 insertions(+) create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 assets/api-responser.svg create mode 100644 composer.json create mode 100644 config/api-responser.php create mode 100644 src/Facades/ApiResponser.php create mode 100644 src/Interfaces/ApiRepositoryInterface.php create mode 100644 src/Providers/ApiResponserServiceProvider.php create mode 100644 src/Repositories/ApiRepository.php create mode 100644 src/Traits/ApiResponser.php create mode 100644 tests/BaseTest.php create mode 100644 tests/Feature/.gitkeep create mode 100644 tests/Feature/ApiResponserShowAllTest.php create mode 100644 tests/Feature/ApiResponserShowOneTest.php create mode 100644 tests/Feature/Traits/ApiResponserTestTrait.php create mode 100644 tests/Models/Post.php create mode 100644 tests/Unit/.gitkeep diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..82176a1 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,30 @@ +name: Tests + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Install dependencies + run: composer install --prefer-dist --no-interaction --no-progress + + - name: Validate PHP PSR + run: composer run-script php-psr + + - name: Run test suite + run: composer run-script test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c329b94 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/vendor/ +/.phpunit.result.cache +/phpunit.xml +/composer.lock +.idea/ +.TODO diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..08af5b9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Multividas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..32225a3 --- /dev/null +++ b/README.md @@ -0,0 +1,127 @@ +
+ +Query Option package logo + +# API Responser + +[![Tests](https://github.com/multividas/api-responser/actions/workflows/tests.yml/badge.svg)](https://github.com/multividas/api-responser/actions/workflows/tests.yml) +[![Total Downloads](https://img.shields.io/packagist/dt/multividas/api-responser.svg?style=flat-square)](https://packagist.org/packages/multividas/api-responser) +[![License](https://img.shields.io/github/license/multividas/api-responser?style=flat-square)](https://github.com/multividas/api-responser/blob/master/LICENSE.md) + +
+ +Composer package to facilitates the process of structuring and generating API responses + +## Installation +Require this package with composer. It is recommended to only require the package for development. + +```shell +composer require multividas/api-responser --dev +``` + +## ServiceProvider: + +**[Optional]** Adding the **ApiResponserServiceProvider** to the providers array in **config/app.php** + +```php +\Multividas\ApiResponser\Providers\ApiResponserServiceProvider::class, +``` + +**[Optional]** To get **X-Application-Name** http response header, Copy the package config to your local config with the publish command: + +```sh +php artisan vendor:publish --tag=api-responser-config +``` + +## Usage + +```php +use \Multividas\ApiResponser\Traits\ApiResponser; + +class Controller extends BaseController +{ + use ApiResponser; +} +``` + +### Dependency Injection + +PostsController has __construct() method initializes a property apiRepository with an instance of the ApiRepositoryInterface. + +`showAll()` method receives **`Collection|JsonResource`** as its param. + +`showOne()` method receives **`Model|JsonResource $instance`** as its param. + +```php +use \Multividas\ApiResponser\Interfaces\ApiRepositoryInterface; + +class PostsController extends Controller +{ + public function __construct( + public ApiRepositoryInterface $apiRepository + ) { + } + + public function index(): JsonResponse + { + return $this->apiRepository->showAll(Post::all()); + } + + public function show(Post $post): JsonResponse + { + if (!$post instanceof Post) { + return $this->infoResponse('Item Not Found', 404, []); + } + + return $this->apiRepository->showOne($post); + } +} +``` + +### Facades + +Using the `ApiResponser` to access the methods of `ApiRepositoryInterface` in your `PostsController`. + +```php +use Multividas\ApiResponser\Facades\ApiResponser; + +class PostsController extends Controller +{ + public function index(): JsonResponse + { + return ApiResponser::showAll(Post::all()); + } + + public function show(string $postId): JsonResponse + { + $post = Post::find($postId); + + if (!$post instanceof Post) { + return $this->infoResponse('Post Not Found', 404, (object)[]); + } + + return ApiResponser::showOne($post); + } +} +``` + +This approach provides a cleaner and more organized way to interact with the `ApiRepositoryInterface` instance in your controller methods. + + +--- + +### Run PHPUnit tests + +```sh +composer test +``` + +--- + +Need helps? Reach us + +> Multividas.com Ⓜ️ + +> Email: contact@multividas.com + +🌌 🚀 diff --git a/assets/api-responser.svg b/assets/api-responser.svg new file mode 100644 index 0000000..fc3be4b --- /dev/null +++ b/assets/api-responser.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..56c615e --- /dev/null +++ b/composer.json @@ -0,0 +1,60 @@ +{ + "name": "multividas/api-responser", + "description": "composer package to facilitates the process of structuring and generating API responses ", + "keywords": [ + "api-responser" + ], + "type": "package", + "license": "MIT", + "authors": [ + { + "name": "multividas inc", + "email": "contact@multividas.com" + } + ], + "autoload": { + "psr-4": { + "Multividas\\ApiResponser\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Multividas\\ApiResponser\\Tests\\": "tests/" + } + }, + "scripts": { + "test": "vendor/bin/phpunit tests/Feature", + "php-psr": [ + "./vendor/bin/phpcs --standard=PSR2 --encoding=utf-8 --extensions=php src/* tests/*" + ] + }, + "minimum-stability": "dev", + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "orchestra/testbench": "dev-develop", + "squizlabs/php_codesniffer": "4.0.x-dev", + "phpunit/phpunit": "9.6.x-dev", + "multividas/query-filters": "dev-main" + }, + "config": { + "optimize-autoloader": true + }, + "extra": { + "laravel": { + "providers": [ + "Multividas\\ApiResponser\\Providers\\ApiResponserServiceProvider" + ], + "aliases": { + "ApiResponser": "Multividas\\ApiResponser\\Facades\\ApiResponser" + } + } + }, + "repositories": [ + { + "type": "path", + "url": "./../query-filters" + } + ] +} diff --git a/config/api-responser.php b/config/api-responser.php new file mode 100644 index 0000000..7ad8814 --- /dev/null +++ b/config/api-responser.php @@ -0,0 +1,13 @@ + Env::get('APP_NAME', 'Api-Responser'), +]; diff --git a/src/Facades/ApiResponser.php b/src/Facades/ApiResponser.php new file mode 100644 index 0000000..2a048a3 --- /dev/null +++ b/src/Facades/ApiResponser.php @@ -0,0 +1,20 @@ +publishes([ + $this->basePath('config/api-responser.php') => base_path('config/api-responser.php') + ], 'api-responser-config'); + } + + public function register(): void + { + $this->mergeConfigFrom($this->basePath('config/api-responser.php'), 'api-responser'); + + $this->app->bind(ApiRepositoryInterface::class, function () { + return new ApiRepository(); + }); + } + + protected function basePath($path = ""): string + { + return __DIR__ . '/../../' . $path; + } +} diff --git a/src/Repositories/ApiRepository.php b/src/Repositories/ApiRepository.php new file mode 100644 index 0000000..cff8cd8 --- /dev/null +++ b/src/Repositories/ApiRepository.php @@ -0,0 +1,65 @@ +isEmpty()) { + return $this->successResponse([ + 'data' => $collection, + 'code' => $code, + 'meta' => (object)$meta ?? [] + ], $code); + } + + $filteredData = QueryFilters::applyFilters($collection); + + return $this->successResponse([ + 'data' => $filteredData['data'], + 'code' => $code, + 'meta' => count($meta) > 0 ? (object)$meta : (object)$filteredData['meta'] + ], $code); + } + + /** + * @JsonResponse instance + */ + public function showOne( + Model|JsonResource $instance, + int $code = 200, + array $meta = [] + ): JsonResponse { + return $this->successResponse([ + 'data' => $instance, + 'code' => $code, + 'meta' => count($meta) > 0 ? (object)$meta : (object)[] + ], $code); + } +} diff --git a/src/Traits/ApiResponser.php b/src/Traits/ApiResponser.php new file mode 100644 index 0000000..5649374 --- /dev/null +++ b/src/Traits/ApiResponser.php @@ -0,0 +1,88 @@ + 'application/json', + 'X-Application-Name' => config('api-responser.app-name'), + ]; + + return new JsonResponse($data, $code, $headers, $this->options); + } + + /** + * Generate an information response. + * + * @param string $message + * @param int $code + * @param array|Collection|EloquentCollection|stdClass $meta + * @return JsonResponse + */ + protected function infoResponse(string $message, int $code, $meta): JsonResponse + { + $data = [ + 'info' => $message, + 'code' => $code, + 'meta' => (object) $meta, + ]; + + $headers = [ + 'Content-Type' => 'application/json', + 'X-Application-Name' => config('api-responser.app-name'), + ]; + + return new JsonResponse($data, $code, $headers, $this->options); + } + + /** + * Handle a validation exception. + * + * @param ValidationException $e + * @return JsonResponse + */ + protected function handleValidationException(ValidationException $e): JsonResponse + { + $errors = $e->validator->errors()->toArray(); + + return $this->infoResponse('The given data was invalid.', 422, ['fields' => $errors]); + } + + /** + * Handle an internal error. + * + * @param Throwable|string $e + * @return JsonResponse + */ + protected function handleInternalError(Throwable|string $e): JsonResponse + { + $message = $e instanceof Throwable ? $e->getMessage() : $e; + + return $this->infoResponse($message, 500, []); + } +} diff --git a/tests/BaseTest.php b/tests/BaseTest.php new file mode 100644 index 0000000..0870371 --- /dev/null +++ b/tests/BaseTest.php @@ -0,0 +1,42 @@ + FacadesApiResponser::class, + 'QueryFilters' => FacadesQueryFilters::class + ]; + } +} diff --git a/tests/Feature/.gitkeep b/tests/Feature/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/Feature/ApiResponserShowAllTest.php b/tests/Feature/ApiResponserShowAllTest.php new file mode 100644 index 0000000..0f914cf --- /dev/null +++ b/tests/Feature/ApiResponserShowAllTest.php @@ -0,0 +1,34 @@ + 1, 'name' => 'John Doe'], + ['id' => 2, 'name' => 'Jane Smith'], + ]; + } + + public function getApiResponse(): JsonResponse + { + $data = $this->arrangeData(); + return ApiResponser::showAll(new Collection($data), 200); + } +} diff --git a/tests/Feature/ApiResponserShowOneTest.php b/tests/Feature/ApiResponserShowOneTest.php new file mode 100644 index 0000000..90e4e08 --- /dev/null +++ b/tests/Feature/ApiResponserShowOneTest.php @@ -0,0 +1,32 @@ + 1, 'name' => 'John Doe']; + } + + public function getApiResponse(): JsonResponse + { + $data = $this->arrangeData(); + $postModel = Post::hydrate([$data])->first(); + return ApiResponser::showOne($postModel, 200); + } +} diff --git a/tests/Feature/Traits/ApiResponserTestTrait.php b/tests/Feature/Traits/ApiResponserTestTrait.php new file mode 100644 index 0000000..362d0c7 --- /dev/null +++ b/tests/Feature/Traits/ApiResponserTestTrait.php @@ -0,0 +1,74 @@ +getApiResponse(); + $responseStatusCode = $response->getStatusCode(); + + $this->assertEquals(200, $responseStatusCode); + } + + /** + * @test + */ + public function testResponseStatusText() + { + $response = $this->getApiResponse(); + $responseStatusText = $response->statusText(); + + $this->assertEquals('OK', $responseStatusText); + } + + /** + * @test + */ + public function testResponseHeaders() + { + $response = $this->getApiResponse(); + $responseHeaders = $response->headers; + + $this->assertEquals('application/json', $responseHeaders->get('Content-Type')); + $this->assertEquals('Api-Responser', $responseHeaders->get('X-Application-Name')); + } + + /** + * @test + */ + public function testResponseJsonContent() + { + $response = $this->getApiResponse(); + $responseJson = $response->getContent(); + + $this->assertJson($responseJson); + } + + /** + * @test + */ + public function testResponseJsonStructure() + { + $response = $this->getApiResponse(); + $responseJson = $response->getContent(); + + $responseData = json_decode($responseJson, true); + + $this->assertArrayHasKey('data', $responseData); + $this->assertArrayHasKey('code', $responseData); + $this->assertArrayHasKey('meta', $responseData); + + $this->assertCount(2, $responseData['data']); + } +} diff --git a/tests/Models/Post.php b/tests/Models/Post.php new file mode 100644 index 0000000..a92a2e7 --- /dev/null +++ b/tests/Models/Post.php @@ -0,0 +1,16 @@ +