From 392fc90b6aa28be3d24fe7d48ffedab5c481758f Mon Sep 17 00:00:00 2001 From: Soulaimane Yahya Date: Sat, 25 May 2024 06:17:24 +0100 Subject: [PATCH] init --- .github/.gitkeep | 0 .github/workflows/tests.yml | 36 +++++ .gitignore | 9 ++ LICENSE | 21 +++ README.md | 146 ++++++++++++++++++ assets/api-responser.svg | 1 + composer.json | 61 ++++++++ config/api-responser.php | 13 ++ src/Facades/ApiResponser.php | 22 +++ src/Interfaces/ApiRepositoryInterface.php | 38 +++++ src/Providers/ApiResponserServiceProvider.php | 39 +++++ src/Repositories/ApiRepository.php | 100 ++++++++++++ src/Traits/ApiResponser.php | 91 +++++++++++ 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 20 files changed, 775 insertions(+) create mode 100755 .github/.gitkeep create mode 100755 .github/workflows/tests.yml create mode 100755 .gitignore create mode 100755 LICENSE create mode 100755 README.md create mode 100755 assets/api-responser.svg create mode 100755 composer.json create mode 100755 config/api-responser.php create mode 100755 src/Facades/ApiResponser.php create mode 100755 src/Interfaces/ApiRepositoryInterface.php create mode 100755 src/Providers/ApiResponserServiceProvider.php create mode 100755 src/Repositories/ApiRepository.php create mode 100755 src/Traits/ApiResponser.php create mode 100755 tests/BaseTest.php create mode 100755 tests/Feature/.gitkeep create mode 100755 tests/Feature/ApiResponserShowAllTest.php create mode 100755 tests/Feature/ApiResponserShowOneTest.php create mode 100755 tests/Feature/Traits/ApiResponserTestTrait.php create mode 100755 tests/Models/Post.php create mode 100755 tests/Unit/.gitkeep diff --git a/.github/.gitkeep b/.github/.gitkeep new file mode 100755 index 0000000..e69de29 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100755 index 0000000..371ca3e --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,36 @@ +name: Tests + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - name: Set up PHP + uses: shivammathur/setup-php@15c43e89cdef867065b0213be354c2841860869e + with: + php-version: "8.2" + + - name: Checkout code + 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 100755 index 0000000..c3becdb --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/vendor/ +/.phpunit.result.cache +/phpunit.xml +/composer.lock +.idea/ +.vscode/* +.githooks/* +.TODO +.php-cs-fixer.cache diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..fe7db9b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 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 100755 index 0000000..6b80ac0 --- /dev/null +++ b/README.md @@ -0,0 +1,146 @@ +
+ +API Responser 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/main/LICENSE) + +
+ +composer package to facilitates the process of structuring and generating API responses + +## Installation +Require this package with composer. + +```shell +composer require multividas/api-responser +``` + +## 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. + +### Success Response + +Successful response containing the requested data and an appropriate status code. + +```json +{ + "data": [], + "code": 200, + "meta": {} +} +``` + +Learn more: [Multividas API Responser](https://developers.multividas.com/rest/introduction/api-responser) + +--- + +### Run PHPUnit tests + +```sh +composer test +``` + +## 🤝 Contributing + +Please read the [contributing guide](https://github.com/multividas/.github/blob/main/CONTRIBUTING.md). + +## 🛡️ Security Issues + +If you discover a security vulnerability within Multividas, we would appreciate your help in disclosing it to us responsibly, please check out our [security issues guidelines](https://github.com/multividas/.github/blob/main/SECURITY.md). + +## 🛡️ License + +Licensed under the [MIT license](https://github.com/multividas/.github/blob/main/LICENSE). + +--- + +> Email: multividasdotcom@gmail.com diff --git a/assets/api-responser.svg b/assets/api-responser.svg new file mode 100755 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 100755 index 0000000..17e36f6 --- /dev/null +++ b/composer.json @@ -0,0 +1,61 @@ +{ + "name": "multividas/api-responser", + "type": "package", + "license": "MIT", + "description": "composer package to facilitates the process of structuring and generating API responses", + "keywords": [ + "api-responser" + ], + "authors": [ + { + "name": "Multividas", + "email": "multividasdotcom@gmail.com" + } + ], + "autoload": { + "psr-4": { + "Multividas\\ApiResponser\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Multividas\\ApiResponser\\Tests\\": "tests/" + } + }, + "scripts": { + "test": "vendor/bin/phpunit tests/Feature", + "php-psr": [ + "find src -type f -name \"*.php\" -print0 | xargs -0 -n1 php -lf", + "vendor/bin/php-cs-fixer fix --allow-risky=yes src --rules=declare_strict_types,@PSR12", + "./vendor/bin/phpcs --standard=PSR2 --encoding=utf-8 --extensions=php src/*" + ] + }, + "require": { + "php": "^8.2" + }, + "require-dev": { + "squizlabs/php_codesniffer": "4.0.x-dev", + "phpunit/phpunit": "9.6.x-dev", + "multividas/query-filters": "dev-main", + "orchestra/testbench": "8.x-dev", + "friendsofphp/php-cs-fixer": "dev-master" + }, + "minimum-stability": "stable", + "config": { + "optimize-autoloader": true + }, + "extra": { + "laravel": { + "providers": [ + "Multividas\\ApiResponser\\Providers\\ApiResponserServiceProvider" + ], + "aliases": { + "ApiResponser": "Multividas\\ApiResponser\\Facades\\ApiResponser" + } + } + }, + "support": { + "issues": "https://github.com/multividas/api-responser/issues", + "source": "https://github.com/multividas/api-responser" + } +} diff --git a/config/api-responser.php b/config/api-responser.php new file mode 100755 index 0000000..44df82e --- /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 100755 index 0000000..351bd61 --- /dev/null +++ b/src/Facades/ApiResponser.php @@ -0,0 +1,22 @@ +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 100755 index 0000000..d2138e1 --- /dev/null +++ b/src/Repositories/ApiRepository.php @@ -0,0 +1,100 @@ +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); + } + + /** + * Method listAll + * + * @param Collection|EloquentCollection|JsonResource $collection + * @param int $code + * @param array $meta + * + * @return JsonResponse + */ + public function listAll( + Collection|EloquentCollection|JsonResource $collection, + int $code = 200, + array $meta = [] + ): JsonResponse { + $cachingData = QueryFilters::cacheData($collection); + + return $this->successResponse([ + 'data' => $cachingData, + 'code' => $code, + 'meta' => (object)$meta ?? [] + ], $code); + } + + /** + * Method showOne + * + * @param Model|JsonResource $instance + * @param int $code + * @param array $meta + * + * @return JsonResponse + */ + 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 100755 index 0000000..b7c9954 --- /dev/null +++ b/src/Traits/ApiResponser.php @@ -0,0 +1,91 @@ + 'application/json', + 'X-Application-Name' => App::make('config')->get('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' => App::make('config')->get('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 100755 index 0000000..5681416 --- /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 100755 index 0000000..e69de29 diff --git a/tests/Feature/ApiResponserShowAllTest.php b/tests/Feature/ApiResponserShowAllTest.php new file mode 100755 index 0000000..3982e85 --- /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 100755 index 0000000..22e2485 --- /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 100755 index 0000000..0d3e328 --- /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 100755 index 0000000..00eaf7e --- /dev/null +++ b/tests/Models/Post.php @@ -0,0 +1,16 @@ +