├── .editorconfig ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── config └── api.php ├── phpstan.neon ├── phpunit.xml ├── pint.json ├── src ├── Data │ └── ApiError.php ├── Factories │ └── HeaderFactory.php ├── Providers │ └── PackageServiceProvider.php └── Responses │ ├── CollectionResponse.php │ ├── ErrorResponse.php │ ├── ExpandedResponse.php │ ├── MessageResponse.php │ └── ModelResponse.php └── tests ├── Feature └── .gitkeep ├── PackageTestCase.php ├── Pest.php └── Unit └── .gitkeep /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml,json,css,js,vue}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.blade.php diff=html 4 | *.css diff=css 5 | *.html diff=html 6 | *.md diff=markdown 7 | *.php diff=php 8 | 9 | /.github export-ignore 10 | CHANGELOG.md export-ignore 11 | .styleci.yml export-ignore 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /node_modules 3 | /public/build 4 | /public/hot 5 | /public/storage 6 | /storage/*.key 7 | /vendor 8 | .env 9 | .env.backup 10 | .env.production 11 | .phpunit.result.cache 12 | Homestead.json 13 | Homestead.yaml 14 | auth.json 15 | npm-debug.log 16 | yarn-error.log 17 | /.fleet 18 | /.idea 19 | /.vscode 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Treblle Limited. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 | # API Responses 7 | 8 | 9 | [![Latest Version][badge-release]][packagist] 10 | [![PHP Version][badge-php]][php] 11 | ![tests](https://github.com/treblle/api-responses/workflows/tests/badge.svg) 12 | [![Total Downloads][badge-downloads]][downloads] 13 | 14 | [badge-release]: https://img.shields.io/packagist/v/treblle/api-responses.svg?style=flat-square&label=release 15 | [badge-php]: https://img.shields.io/packagist/php-v/treblle/api-responses.svg?style=flat-square 16 | [badge-downloads]: https://img.shields.io/packagist/dt/treblle/api-responses.svg?style=flat-square&colorB=mediumvioletred 17 | 18 | [packagist]: https://packagist.org/packages/treblle/api-responses 19 | [php]: https://php.net 20 | [downloads]: https://packagist.org/packages/treblle/api-responses 21 | 22 | 23 | Integrations 24 |   •   25 | Website 26 |   •   27 | Docs 28 |   •   29 | Blog 30 |   •   31 | Twitter 32 |   •   33 | Discord 34 |
35 | 36 |
37 |
38 | 39 | A package to help you keep your API Responses standardized. 40 | 41 | ## Installation 42 | 43 | ```bash 44 | composer require treblle/api-responses 45 | ``` 46 | 47 | ## Usage 48 | 49 | This package is easy to use, it is designed to be used within your controllers to return API responses that are simple and standardized. 50 | 51 | ## Using the configuration 52 | 53 | You can publish the configuration for this package using the following artisan command: 54 | 55 | ```bash 56 | php artisan vendor:publish --tag=api-config 57 | ``` 58 | 59 | This will return the configuration file for this package. Currently, the configuration only covers headers used in responses. 60 | 61 | ```php 62 | return [ 63 | 'headers' => [ 64 | 'default' => [ 65 | 'Content-Type' => 'application/vnd.api+json', 66 | ], 67 | 'error' => [ 68 | 'Content-Type' => 'application/problem+json', 69 | ], 70 | ], 71 | ]; 72 | ``` 73 | 74 | The `HeaderFactory` that is used in the response classes will pull with `HeaderFactory::default()` or `HeaderFactory::error()` depending if you are returning an error or a response. 75 | 76 | You can override the available headers using the configuration file. This is executed outside of any middleware you may be using - which will merge in relevant Headers as required, such as Rate Limiting and Cache headers you may have set. 77 | 78 | ## Returning a single model 79 | 80 | Some API endpoints just need to return a single model, in this situation you should use the `ModelResponse` which accepts a `JsonResource` representation of your model. 81 | 82 | ```php 83 | final class ShowController 84 | { 85 | public function __invoke(Request $request, User $user): Responsable 86 | { 87 | return new ModelResponse( 88 | data: new UserResource( 89 | resource: $user, 90 | ), 91 | ); 92 | } 93 | } 94 | ``` 95 | 96 | ## Returning a collection of models 97 | 98 | Other API endpoints want to return a collection of models, in these situations you should use the `CollectionResponse` which accepts an `AnonymousResourceCollection` which is a collection of Models transformed through API Resources. 99 | 100 | ```php 101 | final class IndexController 102 | { 103 | public function __invoke(Request $request): Responsable 104 | { 105 | return new CollectionResponse( 106 | data: UserResource::collection( 107 | resource: User::query()->get(), 108 | ), 109 | ); 110 | } 111 | } 112 | ``` 113 | 114 | ## When something goes wrong 115 | 116 | The best approach when something goes wrong in your API, the best approach is to allow this to bubble up the your Exception Handler and manage how you respond in one central place. 117 | 118 | ```php 119 | final class Handler extends ExceptionHandler 120 | { 121 | public function register(): void 122 | { 123 | $this->renderable(fn (ModelNotFoundException $exception, Request $request) => new ErrorResponse( 124 | data: new ApiError( 125 | title: 'Not Found', 126 | detail: $exception->getMessage(), 127 | instance: $request->path(), 128 | code: ErrorCode::NOT_FOUND->value, 129 | link: 'https://docs.domain.com/errors/not-found', 130 | ), 131 | status: Status::NOT_FOUND, 132 | )); 133 | } 134 | } 135 | ``` 136 | 137 | ## Sending a simple message response 138 | 139 | Sometimes all you need to do is send a simple message back through your API. Perhaps you are pushing the logic to a background job. 140 | 141 | ```php 142 | final class StoreController 143 | { 144 | public function __invoke(StoreRequest $request): Responsable 145 | { 146 | dispatch(new RegisterProvider($request->payload())); 147 | 148 | return new MessageResponse( 149 | data: 'We have accepted your request, and are processing this action.', 150 | status: Status::ACCEPTED, 151 | ) 152 | } 153 | } 154 | ``` 155 | 156 | ## Sending back a more complex message response 157 | 158 | At times you want to pass back a message as well as some data, perhaps to signify actions that need to be taken. 159 | 160 | ```php 161 | final class LoginController 162 | { 163 | public function __invoke(Request $request): Responsable 164 | { 165 | return new \Treblle\ApiResponses\Responses\ExpandedResponse( 166 | message: __('auth.login'), 167 | data: [ 168 | 'type' => 'login', 169 | 'attributes' => [ 170 | 'mfa' => __('auth.mfa_required'), 171 | ] 172 | ], 173 | ) 174 | } 175 | } 176 | ``` 177 | 178 | ## General Usage 179 | 180 | This package currently contains the following responses: 181 | 182 | - `ModelResponse`: For responding a single model resource. 183 | - `CollectionResponse`: For responding a collection of models for a resource. 184 | - `ErrorResponse`: For responding when you have encountered an Error. 185 | - `MessageResponse`: For when you are returning a simple message. 186 | - `ExpandedResponse`: For when you want to send a message and some data in the response. 187 | 188 | Please note, the `ErrorResponse` is not idea for any `400` responses as these are user errors such as wrong resource or Validation problems. 189 | 190 | 191 | ## Community 💙 192 | 193 | First and foremost: **Star and watch this repository** to stay up-to-date. 194 | 195 | Also, follow our [Blog](https://blog.treblle.com), and on [Twitter](https://twitter.com/treblleapi). 196 | 197 | You can chat with the team and other members on [Discord](https://treblle.com/chat) and follow our tutorials and other video material at [YouTube](https://youtube.com/@treblle). 198 | 199 | [![Treblle Discord](https://img.shields.io/badge/Treblle%20Discord-Join%20our%20Discord-F3F5FC?labelColor=7289DA&style=for-the-badge&logo=discord&logoColor=F3F5FC&link=https://treblle.com/chat)](https://treblle.com/chat) 200 | 201 | [![Treblle YouTube](https://img.shields.io/badge/Treblle%20YouTube-Subscribe%20on%20YouTube-F3F5FC?labelColor=c4302b&style=for-the-badge&logo=YouTube&logoColor=F3F5FC&link=https://youtube.com/@treblle)](https://youtube.com/@treblle) 202 | 203 | [![Treblle on Twitter](https://img.shields.io/badge/Treblle%20on%20Twitter-Follow%20Us-F3F5FC?labelColor=1DA1F2&style=for-the-badge&logo=Twitter&logoColor=F3F5FC&link=https://twitter.com/treblleapi)](https://twitter.com/treblleapi) 204 | 205 | ### How to contribute 206 | 207 | Here are some ways of contributing to making Treblle better: 208 | 209 | - **[Try out Treblle](https://docs.treblle.com/en/introduction#getting-started)**, and let us know ways to make Treblle better for you. Let us know here on [Discord](https://treblle.com/chat). 210 | - Join our [Discord](https://treblle.com/chat) and connect with other members to share and learn from. 211 | - Send a pull request to any of our [open source repositories](https://github.com/Treblle) on Github. Check the contribution guide on the repo you want to contribute to for more details about how to contribute. We're looking forward to your contribution! 212 | - 213 | ## Testing 214 | 215 | To run the test suite: 216 | 217 | ```bash 218 | composer run test 219 | ``` 220 | 221 | ## Credits 222 | 223 | 224 |

225 | A table of avatars from the project's contributors 226 |

227 |
228 | 229 | ## LICENSE 230 | 231 | The MIT LIcense (MIT). Please see [License File](./LICENSE) for more information. 232 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "treblle/api-responses", 3 | "description": "A package to help you keep your API Responses standardized.", 4 | "minimum-stability": "stable", 5 | "prefer-stable": true, 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "role": "Developer", 10 | "name": "Steve McDougall", 11 | "email": "juststevemcd@gmail.com", 12 | "homepage": "https://www.juststeveking.uk/" 13 | } 14 | ], 15 | "autoload": { 16 | "psr-4": { 17 | "Treblle\\ApiResponses\\": "src/" 18 | } 19 | }, 20 | "require": { 21 | "php": "^8.2", 22 | "juststeveking/sdk-tools": "^0.0.5" 23 | }, 24 | "require-dev": { 25 | "laravel/pint": "^1.11", 26 | "orchestra/testbench": "^8.9.1", 27 | "pestphp/pest": "^2.16", 28 | "phpstan/phpstan": "^1.10.32", 29 | "roave/security-advisories": "dev-latest" 30 | }, 31 | "extra": { 32 | "laravel": { 33 | "providers": [ 34 | "Treblle\\ApiResponses\\Providers\\PackageServiceProvider" 35 | ] 36 | } 37 | }, 38 | "scripts": { 39 | "pint": [ 40 | "./vendor/bin/pint" 41 | ], 42 | "stan": [ 43 | "./vendor/bin/phpstan analyse --memory-limit=3g" 44 | ], 45 | "test": [ 46 | "./vendor/bin/pest" 47 | ] 48 | }, 49 | "scripts-descriptions": { 50 | "pint": "Run the Laravel Pint code standards", 51 | "stan": "Run the PhpStan static analysis checks", 52 | "test": "Run the pestPHP test suite" 53 | }, 54 | "config": { 55 | "optimize-autoloader": true, 56 | "preferred-install": "dist", 57 | "sort-packages": true, 58 | "allow-plugins": { 59 | "pestphp/pest-plugin": true, 60 | "php-http/discovery": true 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /config/api.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'default' => [ 8 | 'Content-Type' => 'application/vnd.api+json', 9 | ], 10 | 'error' => [ 11 | 'Content-Type' => 'application/problem+json', 12 | ], 13 | ], 14 | ]; 15 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | paths: 3 | - src/ 4 | 5 | level: 9 6 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests 10 | 11 | 12 | 13 | 14 | ./app 15 | ./src 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "psr12", 3 | "rules": { 4 | "align_multiline_comment": true, 5 | "array_indentation": true, 6 | "array_syntax": true, 7 | "blank_line_after_namespace": true, 8 | "blank_line_after_opening_tag": true, 9 | "combine_consecutive_issets": true, 10 | "combine_consecutive_unsets": true, 11 | "concat_space": { 12 | "spacing": "one" 13 | }, 14 | "declare_parentheses": true, 15 | "declare_strict_types": true, 16 | "explicit_string_variable": true, 17 | "fully_qualified_strict_types": true, 18 | "global_namespace_import": { 19 | "import_classes": true, 20 | "import_constants": true, 21 | "import_functions": true 22 | }, 23 | "is_null": true, 24 | "lambda_not_used_import": true, 25 | "logical_operators": true, 26 | "mb_str_functions": true, 27 | "method_chaining_indentation": true, 28 | "modernize_strpos": true, 29 | "new_with_braces": true, 30 | "no_empty_comment": true, 31 | "not_operator_with_space": true, 32 | "ordered_traits": true, 33 | "protected_to_private": true, 34 | "simplified_if_return": true, 35 | "strict_comparison": true, 36 | "ternary_to_null_coalescing": true, 37 | "trim_array_spaces": true, 38 | "use_arrow_functions": true, 39 | "void_return": true, 40 | "yoda_style": true, 41 | "array_push": true, 42 | "assign_null_coalescing_to_coalesce_equal": true, 43 | "explicit_indirect_variable": true, 44 | "method_argument_space": { 45 | "on_multiline": "ensure_fully_multiline" 46 | }, 47 | "modernize_types_casting": true, 48 | "no_superfluous_elseif": true, 49 | "no_useless_else": true, 50 | "nullable_type_declaration_for_default_null_value": true, 51 | "ordered_imports": { 52 | "sort_algorithm": "alpha" 53 | }, 54 | "ordered_class_elements": { 55 | "order": [ 56 | "use_trait", 57 | "case", 58 | "constant", 59 | "constant_public", 60 | "constant_protected", 61 | "constant_private", 62 | "property_public", 63 | "property_protected", 64 | "property_private", 65 | "construct", 66 | "destruct", 67 | "magic", 68 | "phpunit", 69 | "method_abstract", 70 | "method_public_static", 71 | "method_public", 72 | "method_protected_static", 73 | "method_protected", 74 | "method_private_static", 75 | "method_private" 76 | ], 77 | "sort_algorithm": "none" 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Data/ApiError.php: -------------------------------------------------------------------------------- 1 | $this->title, 50 | 'detail' => $this->detail, 51 | 'instance' => $this->instance, 52 | 'code' => $this->code, 53 | 'link' => $this->link, 54 | ]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Factories/HeaderFactory.php: -------------------------------------------------------------------------------- 1 | $headers 16 | * @return array 17 | */ 18 | public static function default(array $headers = []): array 19 | { 20 | return array_merge( 21 | (array) config('api.headers.default'), 22 | $headers, 23 | ); 24 | } 25 | 26 | /** 27 | * @param array $headers 28 | * @return array 29 | */ 30 | public static function error(array $headers = []): array 31 | { 32 | return array_merge( 33 | (array) config('api.headers.error'), 34 | $headers, 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Providers/PackageServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 14 | $this->publishes([ 15 | __DIR__.'/../../config/api.php' => config_path('api.php'), 16 | ], 'api-config'); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Responses/CollectionResponse.php: -------------------------------------------------------------------------------- 1 | data, 33 | status: $this->status->value, 34 | headers: HeaderFactory::default(), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Responses/ErrorResponse.php: -------------------------------------------------------------------------------- 1 | data->toArray(), 34 | status: $this->status->value, 35 | headers: HeaderFactory::error(), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Responses/ExpandedResponse.php: -------------------------------------------------------------------------------- 1 | $data 17 | * @param Status $status 18 | */ 19 | public function __construct( 20 | private string $message, 21 | private array $data, 22 | private Status $status = Status::OK, 23 | ) { 24 | } 25 | 26 | /** 27 | * @param $request 28 | * @return JsonResponse 29 | */ 30 | public function toResponse($request): JsonResponse 31 | { 32 | return new JsonResponse( 33 | data: [ 34 | 'message' => $this->message, 35 | ...$this->data, 36 | ], 37 | status: $this->status->value, 38 | headers: HeaderFactory::default(), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Responses/MessageResponse.php: -------------------------------------------------------------------------------- 1 | $this->data, 33 | ], 34 | status: $this->status->value, 35 | headers: HeaderFactory::default(), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Responses/ModelResponse.php: -------------------------------------------------------------------------------- 1 | data, 33 | status: $this->status->value, 34 | headers: HeaderFactory::default(), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Feature/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Treblle/api-responses/991a3edbf2336e65b42779a4f1f273e0921d40b5/tests/Feature/.gitkeep -------------------------------------------------------------------------------- /tests/PackageTestCase.php: -------------------------------------------------------------------------------- 1 | in('Feature'); 8 | -------------------------------------------------------------------------------- /tests/Unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Treblle/api-responses/991a3edbf2336e65b42779a4f1f273e0921d40b5/tests/Unit/.gitkeep --------------------------------------------------------------------------------