├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── responder.php ├── resources ├── lang │ └── en │ │ └── errors.php └── stubs │ ├── transformer.model.stub │ └── transformer.plain.stub └── src ├── Console └── MakeTransformer.php ├── Contracts ├── ErrorFactory.php ├── ErrorMessageResolver.php ├── ErrorSerializer.php ├── Pagination │ └── PaginatorFactory.php ├── Resources │ ├── ResourceFactory.php │ └── ResourceKeyResolver.php ├── Responder.php ├── ResponseFactory.php ├── SimpleTransformer.php ├── TransformFactory.php ├── Transformable.php └── Transformers │ └── TransformerResolver.php ├── ErrorFactory.php ├── ErrorMessageResolver.php ├── Exceptions ├── ConvertsExceptions.php ├── Handler.php ├── Http │ ├── HttpException.php │ ├── PageNotFoundException.php │ ├── RelationNotFoundException.php │ ├── UnauthenticatedException.php │ ├── UnauthorizedException.php │ └── ValidationFailedException.php ├── InvalidErrorSerializerException.php ├── InvalidSuccessSerializerException.php └── InvalidTransformerException.php ├── Facades ├── Responder.php └── Transformation.php ├── FractalTransformFactory.php ├── Http ├── MakesResponses.php ├── Middleware │ └── ConvertToSnakeCase.php └── Responses │ ├── Decorators │ ├── EscapeHtmlDecorator.php │ ├── PrettyPrintDecorator.php │ ├── ResponseDecorator.php │ ├── StatusCodeDecorator.php │ └── SuccessFlagDecorator.php │ ├── ErrorResponseBuilder.php │ ├── Factories │ ├── LaravelResponseFactory.php │ └── LumenResponseFactory.php │ ├── ResponseBuilder.php │ └── SuccessResponseBuilder.php ├── Pagination ├── CursorPaginator.php └── PaginatorFactory.php ├── Resources ├── DataNormalizer.php ├── ResourceFactory.php └── ResourceKeyResolver.php ├── Responder.php ├── ResponderServiceProvider.php ├── Serializers ├── ErrorSerializer.php ├── NoopSerializer.php └── SuccessSerializer.php ├── Testing └── MakesApiRequests.php ├── TransformBuilder.php ├── Transformation.php ├── Transformers ├── ArrayTransformer.php ├── Concerns │ ├── HasRelationships.php │ ├── MakesResources.php │ └── OverridesFractal.php ├── Transformer.php └── TransformerResolver.php └── helpers.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 3.3.0 (2023-03-14) 2 | 3 | ### Features 4 | 5 | * Add Laravel 10.0 support 6 | * Add Fractal 0.20 support 7 | 8 | # 3.2.0 (2022-03-25) 9 | 10 | ### Features 11 | 12 | * Add Laravel 9.0 support 13 | 14 | # 3.1.3 (2021-01-07) 15 | 16 | ### Features 17 | 18 | * Add PHP 8.0 support 19 | 20 | # 3.1.2 (2020-09-10) 21 | 22 | ### Features 23 | 24 | * Add Laravel 8.0 support 25 | 26 | # 3.1.1 (2020-03-18) 27 | 28 | ### Bug Fixes 29 | 30 | * Remove typehint from exception handler 31 | 32 | # 3.1.0 (2020-03-09) 33 | 34 | ### Features 35 | 36 | * Add Laravel 7.0 support 37 | 38 | # 3.0.6 (2019-08-28) 39 | 40 | ### Features 41 | 42 | * Add Laravel 6.0 support 43 | 44 | # 3.0.5 (2019-03-01) 45 | 46 | ### Features 47 | 48 | * Add Laravel 5.8 support 49 | 50 | # 3.0.4 (2018-09-05) 51 | 52 | ### Bug Fixes 53 | 54 | * Add missing header call in `ConvertsExceptions` 55 | 56 | # 3.0.3 (2018-09-05) 57 | 58 | ### Features 59 | 60 | * Add Laravel 5.7 support 61 | 62 | # 3.0.2 (2018-02-06) 63 | 64 | ### Bug Fixes 65 | 66 | * Change Collection's `intersectKey` with `array_key_intersect` in transformer as the method isn't available in all Laravel versions 67 | 68 | # 3.0.1 (2018-02-06) 69 | 70 | ### Features 71 | 72 | * When requesting non-whitelisted nested relations, it now returns any relation up to the one that's not whitelisted 73 | * It will now automatically camel case relations before loading them from the model allowing for snake cased relations in transformers 74 | 75 | ### Bug Fixes 76 | 77 | * Fix bug concerning circular relationship mappings 78 | * It will now correctly look for default relations in requested relations that are nested 79 | 80 | # 3.0.0 (2018-01-28) 81 | 82 | Version `3.0.0` contains many bug fixes, but also quite a lot of new features and changes. The entire relationship logic has been rewritten to improve performance, security and stability among other improvements. There has also been big focus on improving test coverage for this release and we're now right below 90% coverage. 83 | 84 | ### Breaking Changes 85 | 86 | * Fractal requirement changed to `0.17.0` 87 | * Whitelisted relationships now requires a transformer mapping in order for eager loading to take effect 88 | * Relationships will now only be eager loaded if you have specified a transformer with whitelisted relationships 89 | * The `transform` method of `Transformer` service and `Transformer` facade has been renamed to `make` and now returns a `TransformBuilder` 90 | * The `Flugg\Responder\Transformer` service has been renamed to `Transformation` 91 | * The `transform` helper function has been renamed to `transformation` 92 | * The `Transformer` facade has been renamed to `Transformation` 93 | * `NullSerializer` has been renamed to `NoopSerializer` 94 | 95 | ### Features 96 | 97 | * New integration test suite 98 | * Added support for primitive resources when including relations 99 | * Added a `fallback_transformer` configuration option to change the fallback transformer 100 | * Added a `error_message_files` configuration option to change translation files to load error messages from 101 | * Support for specifying query constraints for relationships as "load" methods in transformers 102 | * You no longer need to use the `resource` method inside "include" methods in transformers, you can just return the data directly 103 | 104 | ### Bug Fixes 105 | 106 | * It will now only eager load relationships that are whitelisted 107 | * You can now call multiple transformers in sequence without problems 108 | * Associative arrays will now be treated as an item rather than a collection 109 | * A default resource key of `data` is now set, allowing to use the `only` method even when data is empty 110 | 111 | # 2.0.14 (2018-01-23) 112 | 113 | ### Features 114 | 115 | * Added support for Laravel 5.6 116 | 117 | ### Bug Fixes 118 | 119 | * Removed extra end bracket in `PrettyPrintDecorator` 120 | 121 | # 2.0.13 (2018-01-23) 122 | 123 | ### Features 124 | 125 | * Added support for PHP 7.2 126 | * Added two new optional decorators: `PrettyPrintDecorator` and `EscapeHtmlDecorator` 127 | 128 | ### Security Fixes 129 | 130 | * New transformers now has an empty array as whitelisted relations instead of a wildcard 131 | 132 | ### Bug Fixes 133 | 134 | * Parameters are stripped away from relations before eager loading 135 | * Changed `TransformFactory` from singleton to a normal binding 136 | * `NullSerializer` now returns `null` instead of an empty array on null resources 137 | * Relations that have an "include" method in a transformer is no longer eager loaded 138 | 139 | # 2.0.12 (2017-10-17) 140 | 141 | ### Bug Fixes 142 | 143 | * Remove `string` typehint for `$errorCode` 144 | 145 | # 2.0.11 (2017-09-23) 146 | 147 | ### Bug Fixes 148 | 149 | * Change `Responder` and `Transformer` from singletons to regular bindings 150 | 151 | # 2.0.10 (2017-09-17) 152 | 153 | ### Bug Fixes 154 | 155 | * Rebind incompatible translator implementation with Lumen 156 | 157 | # 2.0.9 (2017-09-02) 158 | 159 | ### Bug Fixes 160 | 161 | * Add JSON check to exception handler 162 | 163 | # 2.0.8 (2017-08-17) 164 | 165 | ### Bug Fixes 166 | 167 | * Fix a query string relation parsing bug 168 | 169 | # 2.0.7 (2017-08-17) 170 | 171 | ### Bug Fixes 172 | 173 | * Fix explode default value for query string relations 174 | 175 | # 2.0.6 (2017-08-16) 176 | 177 | ### Bug Fixes 178 | 179 | * Explode query string relations on comma to support multiple relations 180 | 181 | # 2.0.5 (2017-08-16) 182 | 183 | ### Features 184 | 185 | * Automatic resolving of resource key if the data contains models 186 | 187 | ### Bug Fixes 188 | 189 | * Add missing `LogicException` import to base `Transformer` 190 | * Add missing `fields` key to error data of `ValidationFailedException` 191 | 192 | # 2.0.4 (2017-08-15) 193 | 194 | ### Bug Fixes 195 | 196 | * Change `Translator` contract with implementation to widen Laravel support 197 | 198 | # 2.0.3 (2017-08-11) 199 | 200 | ### Bug Fixes 201 | 202 | * Add missing `only` method to `SuccessResponseBuilder` 203 | 204 | # 2.0.2 (2017-08-11) 205 | 206 | ### Bug Fixes 207 | 208 | * Fix null data being converted to arrays for error responses 209 | 210 | # 2.0.1 (2017-08-11) 211 | 212 | ### Bug Fixes 213 | 214 | * Convert empty string messages in `HttpException` to null 215 | * Remove `data` field from error response to make it behave as stated in the documentation 216 | 217 | # 2.0.0 (2017-08-10) 218 | 219 | Version `2.0.0` has been a complete rewrite of the package and brings a lot new stuff to the table, including this very new changelog. The documentation has also been revamped and explains all the new features in greater details. If you're upgrading from an earlier version, make sure to remove your `config/responder.php` file and rerun `php artisan vendor:publish --provider="Flugg\Responder\ResponderServiceProvider"` to publish the new configuration file. 220 | 221 | ### Breaking Changes 222 | 223 | * Fractal requirement changed to `0.16.0` 224 | * Moved `Flugg\Responder\Transformer` to `Flugg\Responder\Transformers\Transformer` 225 | * Changed `Flugg\Responder\Traits\RespondsWithJson` to `Flugg\Responder\Http\Controllers\MakesResponses` 226 | * Changed `Flugg\Responder\Traits\HandlesApiErrors` to `Flugg\Responder\Exceptions\ConvertsExceptions` 227 | * Moved `Flugg\Responder\Traits\MakesApiRequests` to `Flugg\Responder\Testing\MakesApiRequests` 228 | * Removed `Flugg\Responder\Traits\ConvertsParameter`, use new `ConvertToSnakeCase` middleware instead 229 | * Removed `Flugg\Responder\Traits\ThrowsApiErrors`, manually override form requests to replicate 230 | * Changed `Flugg\Responder\Exceptions\Http\ApiException` to `Flugg\Responder\Exceptions\Http\HttpException` 231 | * Renamed `$statusCode` property of the `HttpException` exceptions to `$status` 232 | * Removed `Flugg\Responder\Exceptions\Http\ResourceNotFoundException`, handler now points to `PageNotFoundException` 233 | * Renamed `Flugg\Responder\Serializers\ApiSerializer` to `Flugg\Responder\Serializers\SuccessSerializer` 234 | * Renamed `successResponse` method of the `MakesResponses` trait to `success` 235 | * Renamed `errorResponse` method of the `MakesResponses` trait to `error` 236 | * Return `SuccessResponseBuilder` from `success` method instead of `JsonResponse` 237 | * Return `ErrorResponseBuilder` from `error` method instead of `JsonResponse` 238 | * Renamed `include` method to `with` on `SuccessResponseBuilder` 239 | * Renamed `addMeta` method to `meta` on `SuccessResponseBuilder` 240 | * Removed `transform` method on `SuccessResponseBuilder`, use `success` instead 241 | * Removed `getManager` and `getResource` methods from `SuccessResponseBuilder` 242 | * Changed `transformer` method of the `Transformable` interface to non-static 243 | * Added an `include` prefix to include methods in transformers 244 | * Renamed `transformException` of exception handler trait to `convertDefaultException` 245 | * Renamed `renderApiError` of exception handler trait to `renderResponse` 246 | 247 | ### Features 248 | 249 | * Added configurable response decorators 250 | * Added a `recursion_limit` configuration option 251 | * Allow transforming raw arrays and collections 252 | * Allow sending transformers to the `success` method 253 | * Allow sending resources as data to the `success` method 254 | * Added a `only` method to `SuccessResponseBuilder` to replicate Fractal's `parseFieldsets` 255 | * Added a `cursor` method to `SuccessResponseBuilder` for setting cursors 256 | * Added a `paginator` method to `SuccessResponseBuilder` for setting paginators 257 | * Added a `without` method to `SuccessResponseBuilder` to replicate Fractal's `parseExcludes` 258 | * Relationships are now automatically eager loaded 259 | * Changed `with` method to allow eager loading closures 260 | * Added a `filter_fields_parameter` configuration option for automatic data filtering 261 | * Added a `PageNotFoundException` exception 262 | * Added a `page_not_found` default error code 263 | * Added a `ConvertToSnakeCase` middleware to convert request parameters to snake case 264 | * Added a `Flugg\Responder\Transformer` service to transform without serializing 265 | * Added a `Transformer` facade to transform without serializing 266 | * Added a `transform` helper method to transform without serializing 267 | * Added a `NullSerializer` serializer to serialize without modifying the data 268 | * Added an `ErrorSerializer` contract for serializing errors 269 | * Added a default `Flugg\Responder\Serializers\ErrorSerializer` 270 | * Added a `$load` property to transformers to replicate Fractal's `$defaultIncludes` 271 | * Added a dynamic method in transformers to filter relations: `filterRelationName` 272 | * Allow converting custom exceptions using the `convert` method of the `ConvertsExceptions` trait 273 | * Added a shortcut `-m` to the `--model` modifier of the `make:transformer` command 274 | * Added a `--plain` (and `-p`) option to `make:transformer` to make plain transformers 275 | * Added possibility to bind transformers to models using the `TransformerResolver` class 276 | * Added possibility to bind error messages to error codes using the `ErrorMessageResolver` class 277 | * Decoupled Fractal from the package by introducing a `TransformFactory` adapter 278 | * Changed `success` to transform using an item resource if passed a has-one relation 279 | * Added a `resource` method to the base `Transformer` for creating related resources 280 | 281 | ### Bug Fixes 282 | 283 | * Remove extra field added from deeply nested relations (fixes #33) 284 | * Relations are not eager loaded when automatically including relations (fixes #48) 285 | 286 | ### Performance Improvements 287 | 288 | * Add a new caching layer to transformers, increasing performance with deeply nested relations 289 | * The relation inclusion code has been drastically improved 290 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Alexander Tømmerås 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 all 13 | > 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 THE 21 | > SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | Latest Stable Version 5 | Packagist Downloads 6 | Software License 7 | Build Status 8 | Code Quality 9 | Test Coverage 10 | Donate 11 |

12 | 13 | Laravel Responder is a package for building API responses, integrating [Fractal](https://github.com/thephpleague/fractal) into Laravel and Lumen. It can transform your data using transformers, create and serialize success- and error responses, handle exceptions and assist you with testing your responses. 14 | 15 | # Table of Contents 16 | 17 | - [Introduction](#introduction) 18 | - [Requirements](#requirements) 19 | - [Installation](#installation) 20 | - [Usage](#usage) 21 | - [Creating Responses](#creating-responses) 22 | - [Creating Success Responses](#creating-success-responses) 23 | - [Creating Transformers](#creating-transformers) 24 | - [Transforming Data](#transforming-data) 25 | - [Creating Error Responses](#creating-error-responses) 26 | - [Handling Exceptions](#handling-exceptions) 27 | - [Contributing](#contributing) 28 | - [Donating](#contributing) 29 | - [License](#license) 30 | 31 | # Introduction 32 | 33 | Laravel lets you return models directly from a controller method to convert it to JSON. This is a quick way to build APIs but leaves your database columns exposed. [Fractal](https://fractal.thephpleague.com), a popular PHP package from [The PHP League](https://thephpleague.com/), solves this by introducing transformers. However, it can be a bit cumbersome to integrate into the framework as seen below: 34 | 35 | ```php 36 | public function index() 37 | { 38 | $resource = new Collection(User::all(), new UserTransformer()); 39 | 40 | return response()->json((new Manager)->createData($resource)->toArray()); 41 | } 42 | ``` 43 | 44 | Not _that_ bad, but we all get a little spoiled by Laravel's magic. Wouldn't it be better if we could refactor it to: 45 | 46 | ```php 47 | public function index() 48 | { 49 | return responder()->success(User::all())->respond(); 50 | } 51 | ``` 52 | 53 | The package will allow you to do this and much more. The goal has been to create a high-quality package that feels like native Laravel. A package that lets you embrace the power of Fractal, while hiding it behind beautiful abstractions. There has also been put a lot of focus and thought to the documentation. Happy exploration! 54 | 55 | # Requirements 56 | 57 | This package requires: 58 | - PHP __7.0__+ 59 | - Laravel __5.1__+ or Lumen __5.1__+ 60 | 61 | # Installation 62 | 63 | To get started, install the package through Composer: 64 | 65 | ```shell 66 | composer require flugger/laravel-responder 67 | ``` 68 | 69 | ## Laravel 70 | 71 | #### Register Service Provider 72 | 73 | Append the following line to the `providers` key in `config/app.php` to register the package: 74 | 75 | ```php 76 | Flugg\Responder\ResponderServiceProvider::class, 77 | ``` 78 | 79 | *** 80 | _The package supports auto-discovery, so if you use Laravel 5.5 or later you may skip registering the service provider and facades as they will be registered automatically._ 81 | *** 82 | 83 | #### Register Facades _(optional)_ 84 | 85 | If you like facades, you may also append the `Responder` and `Transformation` facades to the `aliases` key: 86 | 87 | ```php 88 | 'Responder' => Flugg\Responder\Facades\Responder::class, 89 | 'Transformation' => Flugg\Responder\Facades\Transformation::class, 90 | ``` 91 | 92 | #### Publish Package Assets _(optional)_ 93 | 94 | You may additionally publish the package configuration and language file using the `vendor:publish` Artisan command: 95 | 96 | ```shell 97 | php artisan vendor:publish --provider="Flugg\Responder\ResponderServiceProvider" 98 | ``` 99 | 100 | This will publish a `responder.php` configuration file in your `config` folder. It will also publish an `errors.php` file inside your `lang/en` folder which can be used for storing error messages. 101 | 102 | ## Lumen 103 | 104 | #### Register Service Provider 105 | 106 | Add the following line to `app/bootstrap.php` to register the package: 107 | 108 | ```php 109 | $app->register(Flugg\Responder\ResponderServiceProvider::class); 110 | ``` 111 | 112 | #### Register Facades _(optional)_ 113 | 114 | You may also add the following lines to `app/bootstrap.php` to register the facades: 115 | 116 | ```php 117 | class_alias(Flugg\Responder\Facades\Responder::class, 'Responder'); 118 | class_alias(Flugg\Responder\Facades\Transformation::class, 'Transformation'); 119 | ``` 120 | 121 | #### Publish Package Assets _(optional)_ 122 | 123 | Seeing there is no `vendor:publish` command in Lumen, you will have to create your own `config/responder.php` file if you want to configure the package. 124 | 125 | # Usage 126 | 127 | This documentation assumes some knowledge of how [Fractal](https://github.com/thephpleague/fractal) works. 128 | 129 | ## Creating Responses 130 | 131 | The package has a `Responder` service class, which has a `success` and `error` method to build success- and error responses respectively. To use the service and begin creating responses, pick one of the options below: 132 | 133 | #### Option 1: Inject `Responder` Service 134 | 135 | You may inject the `Flugg\Responder\Responder` service class directly into your controller methods: 136 | 137 | ```php 138 | public function index(Responder $responder) 139 | { 140 | return $responder->success(); 141 | } 142 | ``` 143 | 144 | You can also use the `error` method to create error responses: 145 | 146 | ```php 147 | return $responder->error(); 148 | ``` 149 | 150 | #### Option 2: Use `responder` Helper 151 | 152 | If you're a fan of Laravel's `response` helper function, you may like the `responder` helper function: 153 | 154 | ```php 155 | return responder()->success(); 156 | ``` 157 | ```php 158 | return responder()->error(); 159 | ``` 160 | 161 | #### Option 3: Use `Responder` Facade 162 | 163 | Optionally, you may use the `Responder` facade to create responses: 164 | 165 | ```php 166 | return Responder::success(); 167 | ``` 168 | ```php 169 | return Responder::error(); 170 | ``` 171 | 172 | #### Option 4: Use `MakesResponses` Trait 173 | 174 | Lastly, the package provides a `Flugg\Responder\Http\MakesResponses` trait you can use in your controllers: 175 | 176 | ```php 177 | return $this->success(); 178 | ``` 179 | 180 | ```php 181 | return $this->error(); 182 | ``` 183 | 184 | *** 185 | _Which option you pick is up to you, they are all equivalent, the important thing is to stay consistent. The helper function (option 2) will be used for the remaining of the documentation._ 186 | *** 187 | 188 | ### Building Responses 189 | 190 | The `success` and `error` methods return a `SuccessResponseBuilder` and `ErrorResponseBuilder` respectively, which both extend an abstract `ResponseBuilder`, giving them common behaviors. They will be converted to JSON when returned from a controller, but you can explicitly create an instance of `Illuminate\Http\JsonResponse` with the `respond` method: 191 | 192 | ```php 193 | return responder()->success()->respond(); 194 | ``` 195 | 196 | ```php 197 | return responder()->error()->respond(); 198 | ``` 199 | 200 | The status code is set to `200` by default, but can be changed by setting the first parameter. You can also pass a list of headers as the second argument: 201 | 202 | ```php 203 | return responder()->success()->respond(201, ['x-foo' => true]); 204 | ``` 205 | 206 | ```php 207 | return responder()->error()->respond(404, ['x-foo' => false]); 208 | ``` 209 | 210 | *** 211 | _Consider always using the `respond` method for consistency's sake._ 212 | *** 213 | 214 | ### Casting Response Data 215 | 216 | Instead of converting the response to a `JsonResponse` using the `respond` method, you can cast the response data to a few other types, like an array: 217 | 218 | ```php 219 | return responder()->success()->toArray(); 220 | ``` 221 | 222 | ```php 223 | return responder()->error()->toArray(); 224 | ``` 225 | 226 | You also have a `toCollection` and `toJson` method at your disposal. 227 | 228 | ### Decorating Response 229 | 230 | A response decorator allows for last minute changes to the response before it's returned. The package comes with two response decorators out of the box adding a `status` and `success` field to the response output. The `decorators` key in the configuration file defines a list of all enabled response decorators: 231 | 232 | ```php 233 | 'decorators' => [ 234 | \Flugg\Responder\Http\Responses\Decorators\StatusCodeDecorator::class, 235 | \Flugg\Responder\Http\Responses\Decorators\SuccessFlagDecorator::class, 236 | ], 237 | ``` 238 | 239 | You may disable a decorator by removing it from the list, or add your own decorator extending the abstract class `Flugg\Responder\Http\Responses\Decorators\ResponseDecorator`. You can also add additional decorators per response: 240 | 241 | ```php 242 | return responder()->success()->decorator(ExampleDecorator::class)->respond(); 243 | ``` 244 | ```php 245 | return responder()->error()->decorator(ExampleDecorator::class)->respond(); 246 | ``` 247 | 248 | *** 249 | 250 | The package also ships with some situational decorators disabled by default, but which can be added to the decorator list: 251 | - `PrettyPrintDecorator` decorator will beautify the JSON output; 252 | 253 | ```php 254 | \Flugg\Responder\Http\Responses\Decorators\PrettyPrintDecorator::class, 255 | ``` 256 | 257 | - `EscapeHtmlDecorator` decorator, based on the "sanitize input, escape output" concept, will escape HTML entities in all strings returned by your API. You can securely store input data "as is" (even malicious HTML tags) being sure that it will be outputted as un-harmful strings. Note that, using this decorator, printing data as text will result in the wrong representation and you must print it as HTML to retrieve the original value. 258 | 259 | ```php 260 | \Flugg\Responder\Http\Responses\Decorators\EscapeHtmlDecorator::class, 261 | ``` 262 | 263 | *** 264 | 265 | ## Creating Success Responses 266 | 267 | As briefly demonstrated above, success responses are created using the `success` method: 268 | 269 | ```php 270 | return responder()->success()->respond(); 271 | ``` 272 | 273 | Assuming no changes have been made to the configuration, the above code would output the following JSON: 274 | 275 | ```json 276 | { 277 | "status": 200, 278 | "success": true, 279 | "data": null 280 | } 281 | ``` 282 | 283 | ### Setting Response Data 284 | 285 | The `success` method takes the response data as the first argument: 286 | 287 | ```php 288 | return responder()->success(Product::all())->respond(); 289 | ``` 290 | 291 | It accepts the same data types as you would normally return from your controllers, however, it also supports query builder and relationship instances: 292 | 293 | ```php 294 | return responder()->success(Product::where('id', 1))->respond(); 295 | ``` 296 | 297 | ```php 298 | return responder()->success(Product::first()->shipments())->respond(); 299 | ``` 300 | 301 | *** 302 | _The package will run the queries and convert them to collections behind the scenes._ 303 | *** 304 | 305 | ### Transforming Response Data 306 | 307 | The response data will be transformed with Fractal if you've attached a transformer to the response. There are two ways to attach a transformer; either _explicitly_ by setting it on the response, or _implicitly_ by binding it to a model. Let's look at both ways in greater detail. 308 | 309 | #### Setting Transformer On Response 310 | 311 | You can attach a transformer to the response by sending a second argument to the `success` method. For instance, below we're attaching a simple closure transformer, transforming a list of products to only output their names: 312 | 313 | ```php 314 | return responder()->success(Product::all(), function ($product) { 315 | return ['name' => $product->name]; 316 | })->respond(); 317 | ``` 318 | 319 | You may also transform using a dedicated transformer class: 320 | 321 | ```php 322 | return responder()->success(Product::all(), ProductTransformer::class)->respond(); 323 | ``` 324 | 325 | ```php 326 | return responder()->success(Product::all(), new ProductTransformer)->respond(); 327 | ``` 328 | 329 | *** 330 | _You can read more about creating dedicated transformer classes in the [Creating Transformers](#creating-transformers) chapter._ 331 | *** 332 | 333 | #### Binding Transformer To Model 334 | 335 | If no transformer is set, the package will search the response data for an element implementing the `Flugg\Responder\Contracts\Transformable` interface to resolve a transformer from. You can take use of this by implementing the `Transformable` interface in your models: 336 | 337 | ```php 338 | class Product extends Model implements Transformable {} 339 | ``` 340 | 341 | You can satisfy the contract by adding a `transformer` method that returns the corresponding transformer: 342 | 343 | ```php 344 | /** 345 | * Get a transformer for the class. 346 | * 347 | * @return \Flugg\Responder\Transformers\Transformer|string|callable 348 | */ 349 | public function transformer() 350 | { 351 | return ProductTransformer::class; 352 | } 353 | ``` 354 | 355 | *** 356 | _You're not limited to returning a class name string, you can return a transformer instance or closure transformer, just like the second parameter of the `success` method._ 357 | *** 358 | 359 | Instead of implementing the `Transformable` contract for all models, an alternative approach is to bind the transformers using the `bind` method on the `TransformerResolver` class. You can place the code below within `AppServiceProvider` or an entirely new `TransformerServiceProvider`: 360 | 361 | ```php 362 | use Flugg\Responder\Contracts\Transformers\TransformerResolver; 363 | 364 | public function boot() 365 | { 366 | $this->app->make(TransformerResolver::class)->bind([ 367 | \App\Product::class => \App\Transformers\ProductTransformer::class, 368 | \App\Shipment::class => \App\Transformers\ShipmentTransformer::class, 369 | ]); 370 | } 371 | ``` 372 | 373 | After you've bound a transformer to a model you can skip the second parameter and still transform the data: 374 | 375 | ```php 376 | return responder()->success(Product::all())->respond(); 377 | ``` 378 | 379 | *** 380 | _As you might have noticed, unlike Fractal, you don't need to worry about creating resource objects like `Item` and `Collection`. The package will make one for you based on the data type, however, you may wrap your data in a resource object to override this._ 381 | *** 382 | 383 | ### Setting Resource Key 384 | 385 | If the data you send into the response is a model or contains a list of models, a resource key will implicitly be resolved from the model's table name. You can overwrite this by adding a `getResourceKey` method to your model: 386 | 387 | ```php 388 | public function getResourceKey(): string { 389 | return 'products'; 390 | } 391 | ``` 392 | 393 | You can also explicitly set a resource key on a response by sending a third argument to the ´success` method: 394 | 395 | ```php 396 | return responder()->success(Product::all(), ProductTransformer::class, 'products')->respond(); 397 | ``` 398 | 399 | ### Paginating Response Data 400 | 401 | Sending a paginator to the `success` method will set pagination meta data and transform the data automatically, as well as append any query string parameters to the paginator links. 402 | 403 | ```php 404 | return responder()->success(Product::paginate())->respond(); 405 | ``` 406 | 407 | Assuming there are no products and the default configuration is used, the JSON output would look like: 408 | 409 | ```json 410 | { 411 | "success": true, 412 | "status": 200, 413 | "data": [], 414 | "pagination": { 415 | "total": 0, 416 | "count": 0, 417 | "perPage": 15, 418 | "currentPage": 1, 419 | "totalPages": 1, 420 | "links": [] 421 | } 422 | } 423 | ``` 424 | 425 | #### Setting Paginator On Response 426 | 427 | Instead of sending a paginator as data, you may set the data and paginator seperately, like you traditionally would with Fractal. You can manually set a paginator using the `paginator` method, which expects an instance of `League\Fractal\Pagination\IlluminatePaginatorAdapter`: 428 | 429 | ```php 430 | $paginator = Product::paginate(); 431 | $adapter = new IlluminatePaginatorAdapter($paginator); 432 | 433 | return responder()->success($paginator->getCollection())->paginator($adapter)->respond(); 434 | ``` 435 | 436 | #### Setting Cursor On Response 437 | 438 | You can also set cursors using the `cursor` method, expecting an instance of `League\Fractal\Pagination\Cursor`: 439 | 440 | ```php 441 | if ($request->has('cursor')) { 442 | $products = Product::where('id', '>', request()->cursor)->take(request()->limit)->get(); 443 | } else { 444 | $products = Product::take(request()->limit)->get(); 445 | } 446 | 447 | $cursor = new Cursor(request()->cursor, request()->previous, $products->last()->id ?? null, Product::count()); 448 | 449 | return responder()->success($products)->cursor($cursor)->respond(); 450 | ``` 451 | 452 | ### Including Relationships 453 | 454 | If a transformer class is attached to the response, you can include relationships using the `with` method: 455 | 456 | ```php 457 | return responder()->success(Product::all())->with('shipments')->respond(); 458 | ``` 459 | 460 | You can send multiple arguments and specify nested relations using dot notation: 461 | 462 | ```php 463 | return responder()->success(Product::all())->with('shipments', 'orders.customer')->respond(); 464 | ``` 465 | 466 | All relationships will be automatically eager loaded, and just like you would when using `with` or `load` to eager load with Eloquent, you may use a callback to specify additional query constraints. Like in the example below, where we're only including related shipments that hasn't yet been shipped: 467 | 468 | ```php 469 | return responder()->success(Product::all())->with(['shipments' => function ($query) { 470 | $query->whereNull('shipped_at'); 471 | }])->respond(); 472 | ``` 473 | 474 | #### Including From Query String 475 | 476 | Relationships are loaded from a query string parameter if the `load_relations_parameter` configuration key is set to a string. By default, it's set to `with`, allowing you to automatically include relations from the query string: 477 | 478 | ``` 479 | GET /products?with=shipments,orders.customer 480 | ``` 481 | 482 | #### Excluding Default Relations 483 | 484 | In your transformer classes, you may specify relations to automatically load. You may disable any of these relations using the `without` method: 485 | 486 | ```php 487 | return responder()->success(Product::all())->without('comments')->respond(); 488 | ``` 489 | 490 | ### Filtering Transformed Data 491 | 492 | The technique of filtering the transformed data to only return what we need is called sparse fieldsets and can be specified using the `only` method: 493 | 494 | ```php 495 | return responder()->success(Product::all())->only('id', 'name')->respond(); 496 | ``` 497 | 498 | When including relationships, you may also want to filter fields on related resources as well. This can be done by instead specifying an array where each key represents the resource keys for the resources being filtered 499 | 500 | ```php 501 | return responder()->success(Product::all())->with('shipments')->only([ 502 | 'products' => ['id', 'name'], 503 | 'shipments' => ['id'] 504 | ])->respond(); 505 | ``` 506 | 507 | #### Filtering From Query String 508 | 509 | Fields will automatically be filtered if the `filter_fields_parameter` configuration key is set to a string. It defaults to `only`, allowing you to filter fields from the query string: 510 | 511 | ``` 512 | GET /products?only=id,name 513 | ``` 514 | 515 | You may automatically filter related resources by setting the parameter to a key-based array: 516 | 517 | ``` 518 | GET /products?with=shipments&only[products]=id,name&only[shipments]=id 519 | ``` 520 | 521 | ### Adding Meta Data 522 | 523 | You may want to attach additional meta data to your response. You can do this using the `meta` method: 524 | 525 | ```php 526 | return responder()->success(Product::all())->meta(['count' => Product::count()])->respond(); 527 | ``` 528 | 529 | When using the default serializer, the meta data will simply be appended to the response array: 530 | 531 | ```json 532 | { 533 | "success": true, 534 | "status": 200, 535 | "data": [], 536 | "count": 0 537 | } 538 | ``` 539 | 540 | ### Serializing Response Data 541 | 542 | After the data has been transformed, it will be serialized using the specified success serializer in the configuration file, which defaults to the package's own `Flugg\Responder\Serializers\SuccessSerializer`. You can overwrite this on your responses using the `serializer` method: 543 | 544 | ```php 545 | return responder()->success()->serializer(JsonApiSerializer::class)->respond(); 546 | ``` 547 | 548 | ```php 549 | return responder()->success()->serializer(new JsonApiSerializer())->respond(); 550 | ``` 551 | 552 | Above we're using Fractal's `JsonApiSerializer` class. Fractal also ships with an `ArraySerializer` and `DataArraySerializer` class. If none of these suit your taste, feel free to create your own serializer by extending `League\Fractal\Serializer\SerializerAbstract`. You can read more about it in [Fractal's documentation](http://fractal.thephpleague.com/serializers/). 553 | 554 | ## Creating Transformers 555 | 556 | A dedicated transformer class gives you a convenient location to transform data and allows you to reuse the transformer at multiple places. It also allows you to include and transform relationships. You can create a transformer using the `make:transformer` Artisan command: 557 | 558 | ```shell 559 | php artisan make:transformer ProductTransformer 560 | ``` 561 | 562 | The command will generate a new `ProductTransformer.php` file in the `app/Transformers` folder: 563 | 564 | ```php 565 | (int) $product->id, 598 | ]; 599 | } 600 | } 601 | ``` 602 | 603 | It will automatically resolve a model name from the name provided. For instance, the package will extract `Product` from `ProductTransformer` and assume the models live directly in the `app` folder (as per Laravel's convention). If you store them somewhere else, you can use the `--model` (or `-m`) option to override it: 604 | 605 | ```shell 606 | php artisan make:transformer ProductTransformer --model="App\Models\Product" 607 | ``` 608 | 609 | #### Creating Plain Transformers 610 | 611 | The transformer file generated above is a model transformer expecting an `App\Product` model for the `transform` method. However, we can create a plain transformer by applying the `--plain` (or `-p`) modifier: 612 | 613 | ```shell 614 | php artisan make:transformer ProductTransformer --plain 615 | ``` 616 | 617 | This will remove the typehint from the `transform` method and add less boilerplate code. 618 | 619 | ### Setting Relationships 620 | 621 | The `$relations` and `$load` properties in the transformer are the equivalent to Fractal's own `$availableIncludes` and `$defaultIncludes`. In addition to the slight name change, the package uses the `$relations` and `$load` properties to map out all available relationships for eager loading, so in contrast to Fractal, you should map the relationship to their corresponding transformer: 622 | 623 | ```php 624 | protected $relations = [ 625 | 'shipments' => ShipmentTransformer::class, 626 | ]; 627 | ``` 628 | 629 | *** 630 | _You can choose to skip the mapping and just pass the strings like with Fractal, but that means the package wont be able to eager load relationships automatically._ 631 | *** 632 | 633 | #### Setting Whitelisted Relationships 634 | 635 | The `$relations` property specifies a list of relations available to be included. You can set a list of relations mapped to their corresponding transformers: 636 | 637 | ```php 638 | protected $relations = [ 639 | 'shipments' => ShipmentTransformer::class, 640 | 'orders' => OrderTransformer::class, 641 | ]; 642 | ``` 643 | 644 | #### Setting Autoloaded Relationships 645 | 646 | The `$load` property specifies a list of relations to be autoloaded every time you transform data with the transformer: 647 | 648 | ```php 649 | protected $load = [ 650 | 'shipments' => ShipmentTransformer::class, 651 | 'orders' => OrderTransformer::class, 652 | ]; 653 | ``` 654 | 655 | *** 656 | _You don't have to add relations to both `$relations` and `$load`, all relations in `$load` will be available by nature._ 657 | *** 658 | 659 | ### Including Relationships 660 | 661 | While Fractal requires you to to create a method in your transformer for every included relation, this package lets you skip this when transforming models, as it will automatically fetch relationships from the model. You may of course override this functionality by creating an "include" method: 662 | 663 | ```php 664 | /** 665 | * Include related shipments. 666 | * 667 | * @param \App\Product $product 668 | * @return mixed 669 | */ 670 | public function includeShipments(Product $product) 671 | { 672 | return $product->shipments; 673 | } 674 | ``` 675 | 676 | Unlike Fractal you can just return the data directly without wrapping it in an `item` or `collection` method. 677 | 678 | *** 679 | _You should be careful with executing database calls inside the include methods as you might end up with an unexpected amount of hits to the database._ 680 | *** 681 | 682 | #### Using Include Parameters 683 | 684 | Fractal can parse query string parameters which can be used when including relations. For more information about how to format the parameters see Fractal's [documentation on parameters](https://fractal.thephpleague.com/transformers/#include-parameters). You may access the parameters by adding a second parameter to the "include" method: 685 | 686 | ```php 687 | public function includeShipments(Product $product, Collection $parameters) 688 | { 689 | return $product->shipments->take($parameters->get('limit')); 690 | } 691 | ``` 692 | 693 | *** 694 | _To be as decoupled from Fractal as possible the parameters (which are normally accessed using `League\Fractal\ParamBag`) are accessed as Laravel collections instead._ 695 | *** 696 | 697 | #### Adding Query Constraints 698 | 699 | Just as you can specify a query constraint when including a relationship with the `with` method, you can also add query constraints as a "load" method on the transformer. This will automatically be applied when extracting relationships for eager loading. 700 | 701 | ```php 702 | /** 703 | * Load shipments with constraints. 704 | * 705 | * @param \Illuminate\Database\Eloquent\Builder $query 706 | * @return \Illuminate\Database\Eloquent\Builder 707 | */ 708 | public function loadShipments($query) 709 | { 710 | return $query->whereNull('shipped_at'); 711 | } 712 | ``` 713 | 714 | *** 715 | _Note: You cannot mix "include" and "load" methods because the package doesn't eager load relationships included with an "include" method._ 716 | *** 717 | 718 | ### Filtering Relationships 719 | 720 | After a relation has been included, you can make any last second changes to it using a filter method. For instance, below we're filtering the list of related shipments to only include shipments that has not been shipped: 721 | 722 | ```php 723 | /** 724 | * Filter included shipments. 725 | * 726 | * @param \Illuminate\Database\Eloquent\Collection $shipments 727 | * @return \Illuminate\Support\Collection 728 | */ 729 | public function filterShipments($shipments) 730 | { 731 | return $shipments->filter(function ($shipment) { 732 | return is_null($shipment->shipped_at); 733 | }); 734 | } 735 | ``` 736 | 737 | ## Transforming Data 738 | 739 | We've looked at how to transform response data of success responses, however, there may be other places than your controllers where you want to transform data. An example is broadcasted events where you're exposing data using websockets instead of HTTP. You just want to return the transformed data, not an entire response. 740 | 741 | It's possible to simply transform data by newing up the transformer and calling `transform`: 742 | 743 | ```php 744 | return (new ProductTransformer)->transform(Product::first()); 745 | ``` 746 | 747 | However, this approach might become a bit messy when building transformations with relationships: 748 | 749 | ```php 750 | return array_merge((new ProductTransformer)->transform($product = Product::first()), [ 751 | 'shipments' => $product->shipments->map(function ($shipment) { 752 | return (new ShipmentTransformer)->transform($shipment); 753 | }) 754 | ]); 755 | ``` 756 | 757 | Yuck! Imagine that with multiple nested relationships. Let's explore a simpler way to handle this. 758 | 759 | ### Building Transformations 760 | 761 | The `SuccessResponseBuilder` actually delegates all of the transformation work to a dedicated `Flugg\Responder\TransformBuilder` class. We can use this class ourself to transform data. For instance, if the product and shipment transformers were bound to the models, we could replicate the code above in the following way: 762 | 763 | ```php 764 | public function index(TransformBuilder $transformation) 765 | { 766 | return $transformation->resource(Product::all())->with('shipments')->transform(); 767 | } 768 | ``` 769 | 770 | Instead of using the `success` method on the `Responder` service, we use the `resource` method on the `TransformBuilder` with the same method signature. We also use `transform` to execute the transformation instead of `respond` as we did when creating responses. In addition to the `with` method, you also have access to the other transformation methods like `without`, `only`, `meta` and `serializer`. 771 | 772 | *** 773 | _Using `toArray` on the `Responder` service is almost the same as the code above, however, it will also include response decorators which might not be desired._ 774 | *** 775 | 776 | ### Transforming Without Serializing 777 | 778 | When using the `TransformBuilder` to transform data it will still serialize the data using the configured serializer. Fractal requires the use of a serializer to transform data, but sometimes we're just interested in the raw transformed data. The package ships with a `Flugg\Responder\Serializers\NoopSerializer` to solve this, a no-op serializer which leaves the transformed data untouched: 779 | 780 | ```php 781 | return $transformation->resource(Product::all())->serializer(NoopSerializer::class)->transform(); 782 | ``` 783 | 784 | If you think this is looking messy, don't worry, there's a quicker way. In fact, you will probably never even need to utilize the `NoopSerializer` or `TransformBuilder` manually, but it helps to know how it works. The `Flugg\Responder\Transformation` is a class which can be used for quickly transforming data without serializing. 785 | 786 | #### Option 1: The `Transformation` Service 787 | 788 | The `Transformation` class utilizes the `TransformBuilder` class to build a transformation using the `NoopSerializer`. You can inject the `Transformation` class and call `make` to obtain an instance of `TransformBuilder` which gives you access to all of the chainable methods including `with`, like below: 789 | 790 | ```php 791 | public function __construct(Transformation $transformation) 792 | { 793 | $transformation->make(Product::all())->with('shipments')->transform(); 794 | } 795 | ``` 796 | 797 | #### Option 2: The `transformation` Helper 798 | 799 | You can use the `transformation` helper function to transform data without serializing: 800 | 801 | ```php 802 | transformation(Product::all())->with('shipments')->transform(); 803 | ``` 804 | 805 | #### Option 3: The `Transformation` Facade 806 | 807 | You can also use the `Transformation` facade to achieve the same thing: 808 | 809 | ```php 810 | Transformation::make(Product::all())->with('shipments')->transform(); 811 | ``` 812 | 813 | ### Transforming To Camel Case 814 | 815 | Model attributes are traditionally specified in snake case, however, you might prefer to use camel case for the response fields. A transformer makes for a perfect location to convert the fields, as seen from the `soldOut` field in the example below: 816 | 817 | ```php 818 | return responder()->success(Product::all(), function ($product) { 819 | return ['soldOut' => (bool) $product->sold_out]; 820 | })->respond(); 821 | ``` 822 | 823 | #### Transforming Request Parameters 824 | 825 | After responding with camel case, you probably want to let people send in request data using camel cased parameters as well. The package provides a `Flugg\Responder\Http\Middleware\ConvertToSnakeCase` middleware you can append to the `$middleware` array in `app/Http/Kernel.php` to convert all parameters to snake case automatically: 826 | 827 | ```php 828 | protected $middleware = [ 829 | // ... 830 | \Flugg\Responder\Http\Middleware\ConvertToSnakeCase::class, 831 | ]; 832 | ``` 833 | 834 | *** 835 | _The middleware will run before request validation, so you should specify your validation rules in snake case as well._ 836 | *** 837 | 838 | ## Creating Error Responses 839 | 840 | Whenever a consumer of your API does something unexpected, you can return an error response describing the problem. As briefly shown in a previous chapter, an error response can be created using the `error` method: 841 | 842 | ```php 843 | return responder()->error()->respond(); 844 | ``` 845 | 846 | The error response has knowledge about an error code, a corresponding error message and optionally some error data. With the default configuration, the above code would output the following JSON: 847 | 848 | ```json 849 | { 850 | "success": false, 851 | "status": 500, 852 | "error": { 853 | "code": null, 854 | "message": null 855 | } 856 | } 857 | ``` 858 | 859 | ### Setting Error Code & Message 860 | 861 | You can fill the first parameter of the `error` method to set an error code: 862 | 863 | ```php 864 | return responder()->error('sold_out_error')->respond(); 865 | ``` 866 | 867 | *** 868 | _You may optionally use integers for error codes._ 869 | *** 870 | 871 | In addition, you may set the second parameter to an error message describing the error: 872 | 873 | ```php 874 | return responder()->error('sold_out_error', 'The requested product is sold out.')->respond(); 875 | ``` 876 | 877 | #### Set Messages In Language Files 878 | 879 | You can set the error messages in a language file, which allows for returning messages in different languages. The configuration file has an `error_message_files` key defining a list of language files with error messages. By default, it is set to `['errors']`, meaning it will look for an `errors.php` file inside `resources/lang/en`. You can use these files to map error codes to corresponding error messages: 880 | 881 | ```php 882 | return [ 883 | 'sold_out_error' => 'The requested product is sold out.', 884 | ]; 885 | ``` 886 | 887 | #### Register Messages Using `ErrorMessageResolver` 888 | 889 | Instead of using language files, you may alternatively set error messages directly on the `ErrorMessageResolver` class. You can place the code below within `AppServiceProvider` or an entirely new `TransformerServiceProvider`: 890 | 891 | ```php 892 | use Flugg\Responder\ErrorMessageResolver; 893 | 894 | public function boot() 895 | { 896 | $this->app->make(ErrorMessageResolver::class)->register([ 897 | 'sold_out_error' => 'The requested product is sold out.', 898 | ]); 899 | } 900 | ``` 901 | 902 | ### Adding Error Data 903 | 904 | You may want to set additional data on the error response. Like in the example below, we're returning a list of shipments with the `sold_out` error response, giving the consumer information about when a new shipment for the product might arrive. 905 | 906 | ```php 907 | return responder()->error('sold_out')->data(['shipments' => Shipment::all()])->respond(); 908 | ``` 909 | 910 | The error data will be appended to the response data. Assuming we're using the default serializer and there are no shipments in the database, the code above would look like: 911 | 912 | ```json 913 | { 914 | "success": false, 915 | "status": 500, 916 | "error": { 917 | "code": "sold_out", 918 | "message": "The requested product is sold out.", 919 | "shipments": [] 920 | } 921 | } 922 | ``` 923 | 924 | ### Serializing Response Data 925 | 926 | Similarly to success responses, error responses will be serialized using the specified error serializer in the configuration file. This defaults to the package's own `Flugg\Responder\Serializers\ErrorSerializer`, but can of course be changed by using the `serializer` method: 927 | 928 | ```php 929 | return responder()->error()->serializer(ExampleErrorSerializer::class)->respond(); 930 | ``` 931 | 932 | ```php 933 | return responder()->success()->serializer(new ExampleErrorSerializer())->respond(); 934 | ``` 935 | 936 | You can create your own error serializer by implementing the `Flugg\Responder\Contracts\ErrorSerializer` contract. 937 | 938 | ## Handling Exceptions 939 | 940 | No matter how much we try to avoid them, exceptions do happen. Responding to the exceptions in an elegant manner will improve the user experience of your API. The package can enhance your exception handler to automatically turn exceptions in to error responses. If you want to take use of this, you can either use the package's exception handler or include a trait as described in further details below. 941 | 942 | #### Option 1: Replace `Handler` Class 943 | 944 | To use the package's exception handler you need to replace the default import in `app/Exceptions/Handler.php`: 945 | 946 | ```php 947 | use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; 948 | ``` 949 | 950 | With the package's handler class: 951 | 952 | ```php 953 | use Flugg\Responder\Exceptions\Handler as ExceptionHandler; 954 | ``` 955 | 956 | *** 957 | This will not work with Lumen as its exception handler is incompatible with Laravel's. Look instead at the second option below. 958 | *** 959 | 960 | #### Option 2: Use `ConvertsExceptions` Trait 961 | 962 | The package's exception handler uses the `Flugg\Responder\Exceptions\ConvertsExceptions` trait to load of most of its work. Instead of replacing the exception handler, you can use the trait in your own handler class. To replicate the behavior of the exception handler, you would also have to add the following code to the `render` method: 963 | 964 | ```php 965 | public function render($request, Exception $exception) 966 | { 967 | $this->convertDefaultException($exception); 968 | 969 | if ($exception instanceof HttpException) { 970 | return $this->renderResponse($exception); 971 | } 972 | 973 | return parent::render($request, $exception); 974 | } 975 | ``` 976 | 977 | If you only want to return JSON error responses on requests actually asking for JSON, you may wrap the code above in a `wantsJson` check as seen below: 978 | 979 | ```php 980 | if ($request->wantsJson()) { 981 | $this->convertDefaultException($exception); 982 | 983 | if ($exception instanceof HttpException) { 984 | return $this->renderResponse($exception); 985 | } 986 | } 987 | ``` 988 | 989 | ### Converting Exceptions 990 | 991 | Once you've implemented one of the above options, the package will convert some of Laravel's exceptions to an exception extending `Flugg\Responder\Exceptions\Http\HttpException`. It will then convert these to an error response. The table below shows which Laravel exceptions are converted and what they are converted to. All the exceptions on the right is under the `Flugg\Responder\Exceptions\Http` namespace and extends `Flugg\Responder\Exceptions\Http\HttpException`. All exceptions extending the `HttpException` class will be automatically converted to an error response. 992 | 993 | | Caught Exceptions | Converted To | 994 | | --------------------------------------------------------------- | ---------------------------- | 995 | | `Illuminate\Auth\AuthenticationException` | `UnauthenticatedException` | 996 | | `Illuminate\Auth\Access\AuthorizationException` | `UnauthorizedException` | 997 | | `Symfony\Component\HttpKernel\Exception\NotFoundHttpException` | `PageNotFoundException` | 998 | | `Illuminate\Database\Eloquent\ModelNotFoundException` | `PageNotFoundException` | 999 | | `Illuminate\Database\Eloquent\RelationNotFoundException` | `RelationNotFoundException` | 1000 | | `Illuminate\Validation\ValidationException` | `ValidationFailedException` | 1001 | 1002 | You can disable the conversions of some of the exceptions above using the `$dontConvert` property: 1003 | 1004 | ```php 1005 | /** 1006 | * A list of default exception types that should not be converted. 1007 | * 1008 | * @var array 1009 | */ 1010 | protected $dontConvert = [ 1011 | ModelNotFoundException::class, 1012 | ]; 1013 | ``` 1014 | 1015 | *** 1016 | If you're using the trait option, you can disable all the default conversions by removing the call to `convertDefaultException` in the `render` method. 1017 | *** 1018 | 1019 | #### Convert Custom Exceptions 1020 | 1021 | In addition to letting the package convert Laravel exceptions, you can also convert your own exceptions using the `convert` method in the `render` method: 1022 | 1023 | ```php 1024 | $this->convert($exception, [ 1025 | InvalidValueException => PageNotFoundException, 1026 | ]); 1027 | ``` 1028 | 1029 | You can optionally give it a closure that throws the new exception, if you want to give it constructor parameters: 1030 | 1031 | ```php 1032 | $this->convert($exception, [ 1033 | MaintenanceModeException => function ($exception) { 1034 | throw new ServerDownException($exception->retryAfter); 1035 | }, 1036 | ]); 1037 | ``` 1038 | 1039 | ### Creating HTTP Exceptions 1040 | 1041 | An exception class is a convenient place to store information about an error. The package provides an abstract exception class `Flugg\Responder\Exceptions\Http\HttpException`, which has knowledge about status code, an error code and an error message. Continuing on our product example from above, we could create our own `HttpException` class: 1042 | 1043 | ```php 1044 | Shipment::all() 1087 | ]; 1088 | } 1089 | ``` 1090 | 1091 | If you're letting the package handle exceptions, you can now throw the exception anywhere in your application and it will automatically be rendered to an error response. 1092 | 1093 | ```php 1094 | throw new SoldOutException(); 1095 | ``` 1096 | 1097 | # Contributing 1098 | 1099 | Contributions are more than welcome and you're free to create a pull request on Github. You can run tests with the following command: 1100 | 1101 | ```shell 1102 | vendor/bin/phpunit 1103 | ``` 1104 | 1105 | If you find bugs or have suggestions for improvements, feel free to submit an issue on Github. However, if it's a security related issue, please send an email to flugged@gmail.com instead. 1106 | 1107 | # Donating 1108 | 1109 | The package is completely free to use, however, a lot of time has been put into making it. If you want to show your appreciation by leaving a small donation, you can do so by clicking [here](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=PRMC9WLJY8E46&lc=NO&item_name=Laravel%20Responder¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted). Thanks! 1110 | 1111 | # License 1112 | 1113 | Laravel Responder is free software distributed under the terms of the MIT license. See [license.md](license.md) for more details. 1114 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flugger/laravel-responder", 3 | "description": "A Laravel Fractal package for building API responses, giving you the power of Fractal and the elegancy of Laravel.", 4 | "keywords": [ 5 | "laravel", 6 | "lumen", 7 | "fractal", 8 | "transformer", 9 | "api", 10 | "response", 11 | "responder" 12 | ], 13 | "homepage": "https://github.com/flugger/laravel-responder", 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Alexander Tømmerås", 18 | "email": "flugged@gmail.com" 19 | } 20 | ], 21 | "require": { 22 | "php": "^7.4|^8.0", 23 | "illuminate/contracts": "^5.1|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 24 | "illuminate/support": "^5.1|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 25 | "league/fractal": "^0.19.0|^0.20" 26 | }, 27 | "require-dev": { 28 | "illuminate/database": "^5.1|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 29 | "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0|^10.0", 30 | "mockery/mockery": "^0.9.5|^1.0", 31 | "doctrine/dbal": "^2.5|^3.5|^4.2", 32 | "phpunit/phpunit": "^8.5|^9.0|^10.5|^11.5.3" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Flugg\\Responder\\": "src" 37 | }, 38 | "files": [ 39 | "src/helpers.php" 40 | ] 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "Flugg\\Responder\\Tests\\": "tests" 45 | } 46 | }, 47 | "config": { 48 | "sort-packages": true 49 | }, 50 | "extra": { 51 | "laravel": { 52 | "providers": [ 53 | "Flugg\\Responder\\ResponderServiceProvider" 54 | ], 55 | "aliases": { 56 | "Responder": "Flugg\\Responder\\Facades\\Responder", 57 | "Transformer": "Flugg\\Responder\\Facades\\Transformer" 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /config/responder.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'success' => Flugg\Responder\Serializers\SuccessSerializer::class, 18 | 'error' => \Flugg\Responder\Serializers\ErrorSerializer::class, 19 | ], 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Response Decorators 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Response decorators are used to decorate both your success- and error 27 | | responses. A decorator can be disabled by removing it from the list 28 | | below. You may additionally add your own decorators to the list. 29 | | 30 | */ 31 | 32 | 'decorators' => [ 33 | \Flugg\Responder\Http\Responses\Decorators\StatusCodeDecorator::class, 34 | \Flugg\Responder\Http\Responses\Decorators\SuccessFlagDecorator::class, 35 | ], 36 | 37 | /* 38 | |-------------------------------------------------------------------------- 39 | | Fallback Transformer 40 | |-------------------------------------------------------------------------- 41 | | 42 | | When transforming data without specifying a transformer we'll instead 43 | | use a fallback transformer specified below. The [ArrayTransformer] 44 | | transformer will simply convert the data to an array untouched. 45 | | 46 | */ 47 | 48 | 'fallback_transformer' => \Flugg\Responder\Transformers\ArrayTransformer::class, 49 | 50 | /* 51 | |-------------------------------------------------------------------------- 52 | | Load Relationships With Query String Parameter 53 | |-------------------------------------------------------------------------- 54 | | 55 | | The package can automatically load relationships from the query string 56 | | and will look for a query string parameter with the name configured 57 | | below. You can set the value to null to disable the autoloading. 58 | | 59 | */ 60 | 61 | 'load_relations_parameter' => 'with', 62 | 63 | /* 64 | |-------------------------------------------------------------------------- 65 | | Filter Fields With Query String Parameter 66 | |-------------------------------------------------------------------------- 67 | | 68 | | The package can automatically filter the fields of transformed data 69 | | from a query string parameter configured below. The technique is 70 | | also known as sparse fieldsets. Set it to null to disable it. 71 | | 72 | */ 73 | 74 | 'filter_fields_parameter' => 'only', 75 | 76 | /* 77 | |-------------------------------------------------------------------------- 78 | | Recursion Limit 79 | |-------------------------------------------------------------------------- 80 | | 81 | | When transforming data, you may be including relations recursively. 82 | | By setting the value below, you can limit the amount of times it 83 | | should include relationships recursively. Five might be good. 84 | | 85 | */ 86 | 87 | 'recursion_limit' => 5, 88 | 89 | /* 90 | |-------------------------------------------------------------------------- 91 | | Error Message Translation Files 92 | |-------------------------------------------------------------------------- 93 | | 94 | | You can declare error messages in a language file, which allows for 95 | | returning messages in different languages. The array below lists 96 | | the language files that will be searched in to find messages. 97 | | 98 | */ 99 | 100 | 'error_message_files' => ['errors'], 101 | 102 | /* 103 | |-------------------------------------------------------------------------- 104 | | CamelCase Relations 105 | |-------------------------------------------------------------------------- 106 | | 107 | | By default laravel responder will convert relations request to camel-case 108 | | but some people would like to use snake-case, so you can set it below 109 | | 110 | */ 111 | 112 | 'use_camel_case_relations' => true, 113 | 114 | ]; -------------------------------------------------------------------------------- /resources/lang/en/errors.php: -------------------------------------------------------------------------------- 1 | 'You are not authenticated for this request.', 17 | 'unauthorized' => 'You are not authorized for this request.', 18 | 'page_not_found' => 'The requested page does not exist.', 19 | 'relation_not_found' => 'The requested relation does not exist.', 20 | 'validation_failed' => 'The given data failed to pass validation.', 21 | 22 | ]; -------------------------------------------------------------------------------- /resources/stubs/transformer.model.stub: -------------------------------------------------------------------------------- 1 | (int) $DummyModelVariable->id, 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /resources/stubs/transformer.plain.stub: -------------------------------------------------------------------------------- 1 | 15 | * @license The MIT License 16 | */ 17 | class MakeTransformer extends GeneratorCommand 18 | { 19 | /** 20 | * The console command name. 21 | * 22 | * @var string 23 | */ 24 | protected $name = 'make:transformer'; 25 | 26 | /** 27 | * The console command description. 28 | * 29 | * @var string 30 | */ 31 | protected $description = 'Create a new transformer class'; 32 | 33 | /** 34 | * The type of class being generated. 35 | * 36 | * @var string 37 | */ 38 | protected $type = 'Transformer'; 39 | 40 | /** 41 | * Get the stub file for the generator. 42 | * 43 | * @return string 44 | */ 45 | protected function getStub() 46 | { 47 | if ($this->option('plain')) { 48 | return __DIR__ . '/../../resources/stubs/transformer.plain.stub'; 49 | } 50 | 51 | return __DIR__ . '/../../resources/stubs/transformer.model.stub'; 52 | } 53 | 54 | /** 55 | * Get the default namespace for the class. 56 | * 57 | * @param string $rootNamespace 58 | * @return string 59 | */ 60 | protected function getDefaultNamespace($rootNamespace) 61 | { 62 | return $rootNamespace . '\Transformers'; 63 | } 64 | 65 | /** 66 | * Build the class with the given name. 67 | * 68 | * @param string $name 69 | * @return string 70 | */ 71 | protected function buildClass($name) 72 | { 73 | $replace = []; 74 | 75 | if (! $this->option('model') && ! $this->option('plain')) { 76 | $this->input->setOption('model', $this->resolveModelFromClassName()); 77 | } 78 | 79 | if ($this->option('model')) { 80 | $replace = $this->buildModelReplacements($replace); 81 | } 82 | 83 | return str_replace(array_keys($replace), array_values($replace), parent::buildClass($name)); 84 | } 85 | 86 | /** 87 | * Resolve a model from the given class name. 88 | * 89 | * @return string 90 | */ 91 | protected function resolveModelFromClassName() 92 | { 93 | return 'App\\Models\\' . str_replace('Transformer', '', Arr::last(explode('/', $this->getNameInput()))); 94 | } 95 | 96 | /** 97 | * Build the model replacement values. 98 | * 99 | * @param array $replace 100 | * @return array 101 | */ 102 | protected function buildModelReplacements(array $replace) 103 | { 104 | if (! class_exists($modelClass = $this->parseModel($this->option('model')))) { 105 | if ($this->confirm("A {$modelClass} model does not exist. Do you want to generate it?", true)) { 106 | $this->call('make:model', ['name' => $modelClass]); 107 | } 108 | } 109 | 110 | return array_merge($replace, [ 111 | 'DummyFullModelClass' => $modelClass, 112 | 'DummyModelClass' => class_basename($modelClass), 113 | 'DummyModelVariable' => lcfirst(class_basename($modelClass)), 114 | ]); 115 | } 116 | 117 | /** 118 | * Get the fully-qualified model class name. 119 | * 120 | * @param string $model 121 | * @return string 122 | */ 123 | protected function parseModel($model) 124 | { 125 | if (preg_match('([^A-Za-z0-9_/\\\\])', $model)) { 126 | throw new InvalidArgumentException('Model name contains invalid characters.'); 127 | } 128 | 129 | $model = trim(str_replace('/', '\\', $model), '\\'); 130 | 131 | if (! Str::startsWith($model, $rootNamespace = $this->laravel->getNamespace())) { 132 | $model = $rootNamespace . $model; 133 | } 134 | 135 | return $model; 136 | } 137 | 138 | /** 139 | * Get the console command options. 140 | * 141 | * @return array 142 | */ 143 | protected function getOptions() 144 | { 145 | return [ 146 | ['model', 'm', InputOption::VALUE_OPTIONAL, 'Generate a model transformer.'], 147 | ['plain', 'p', InputOption::VALUE_NONE, 'Generate a plain transformer.'], 148 | ]; 149 | } 150 | } -------------------------------------------------------------------------------- /src/Contracts/ErrorFactory.php: -------------------------------------------------------------------------------- 1 | 9 | * @license The MIT License 10 | */ 11 | interface ErrorFactory 12 | { 13 | /** 14 | * Make an error array from the given error code, message and error data. 15 | * 16 | * @param \Flugg\Responder\Contracts\ErrorSerializer $serializer 17 | * @param mixed|null $errorCode 18 | * @param string|null $message 19 | * @param array|null $data 20 | * @return array 21 | */ 22 | public function make(ErrorSerializer $serializer, $errorCode = null, ?string $message = null, ?array $data = null): array; 23 | } 24 | -------------------------------------------------------------------------------- /src/Contracts/ErrorMessageResolver.php: -------------------------------------------------------------------------------- 1 | 10 | * @license The MIT License 11 | */ 12 | interface ErrorMessageResolver 13 | { 14 | /** 15 | * Resolve a message from the given error code. 16 | * 17 | * @param mixed $errorCode 18 | * @return string|null 19 | */ 20 | public function resolve($errorCode); 21 | } -------------------------------------------------------------------------------- /src/Contracts/ErrorSerializer.php: -------------------------------------------------------------------------------- 1 | 9 | * @license The MIT License 10 | */ 11 | interface ErrorSerializer 12 | { 13 | /** 14 | * Format the error data. 15 | * 16 | * @param mixed|null $errorCode 17 | * @param string|null $message 18 | * @param array|null $data 19 | * @return array 20 | */ 21 | public function format($errorCode = null, ?string $message = null, ?array $data = null): array; 22 | } 23 | -------------------------------------------------------------------------------- /src/Contracts/Pagination/PaginatorFactory.php: -------------------------------------------------------------------------------- 1 | 15 | * @license The MIT License 16 | */ 17 | interface PaginatorFactory 18 | { 19 | /** 20 | * Make a Fractal paginator adapter from a Laravel paginator. 21 | * 22 | * @param \Illuminate\Contracts\Pagination\LengthAwarePaginator $paginator 23 | * @return \League\Fractal\Pagination\PaginatorInterface 24 | */ 25 | public function make(LengthAwarePaginator $paginator): PaginatorInterface; 26 | 27 | /** 28 | * Make a Fractal paginator adapter from a Laravel paginator. 29 | * 30 | * @param \Flugg\Responder\Pagination\CursorPaginator $paginator 31 | * @return \League\Fractal\Pagination\Cursor 32 | */ 33 | public function makeCursor(CursorPaginator $paginator): Cursor; 34 | } -------------------------------------------------------------------------------- /src/Contracts/Resources/ResourceFactory.php: -------------------------------------------------------------------------------- 1 | 11 | * @license The MIT License 12 | */ 13 | interface ResourceFactory 14 | { 15 | /** 16 | * Make resource from the given data. 17 | * 18 | * @param mixed $data 19 | * @param \Flugg\Responder\Transformers\Transformer|string|callable|null $transformer 20 | * @param string|null $resourceKey 21 | * @return \League\Fractal\Resource\ResourceInterface 22 | */ 23 | public function make($data = null, $transformer = null, ?string $resourceKey = null): ResourceInterface; 24 | } 25 | -------------------------------------------------------------------------------- /src/Contracts/Resources/ResourceKeyResolver.php: -------------------------------------------------------------------------------- 1 | 10 | * @license The MIT License 11 | */ 12 | interface ResourceKeyResolver 13 | { 14 | /** 15 | * Register a transformable to resource key binding. 16 | * 17 | * @param string|array $transformable 18 | * @param string $resourceKey 19 | * @return void 20 | */ 21 | public function bind($transformable, string $resourceKey); 22 | 23 | /** 24 | * Resolve a resource key from the given data. 25 | * 26 | * @param mixed $data 27 | * @return string 28 | */ 29 | public function resolve($data); 30 | } -------------------------------------------------------------------------------- /src/Contracts/Responder.php: -------------------------------------------------------------------------------- 1 | 12 | * @license The MIT License 13 | */ 14 | interface Responder 15 | { 16 | /** 17 | * Build a successful response. 18 | * 19 | * @param mixed $data 20 | * @param callable|string|\Flugg\Responder\Transformers\Transformer|null $transformer 21 | * @param string|null $resourceKey 22 | * @return \Flugg\Responder\Http\Responses\SuccessResponseBuilder 23 | */ 24 | public function success($data = null, $transformer = null, ?string $resourceKey = null): SuccessResponseBuilder; 25 | 26 | /** 27 | * Build an error response. 28 | * 29 | * @param mixed|null $errorCode 30 | * @param string|null $message 31 | * @return \Flugg\Responder\Http\Responses\ErrorResponseBuilder 32 | */ 33 | public function error($errorCode = null, ?string $message = null): ErrorResponseBuilder; 34 | } 35 | -------------------------------------------------------------------------------- /src/Contracts/ResponseFactory.php: -------------------------------------------------------------------------------- 1 | 12 | * @license The MIT License 13 | */ 14 | interface ResponseFactory 15 | { 16 | /** 17 | * Generate a JSON response. 18 | * 19 | * @param array $data 20 | * @param int $status 21 | * @param array $headers 22 | * @return \Illuminate\Http\JsonResponse 23 | */ 24 | public function make(array $data, int $status, array $headers = []): JsonResponse; 25 | } -------------------------------------------------------------------------------- /src/Contracts/SimpleTransformer.php: -------------------------------------------------------------------------------- 1 | 11 | * @license The MIT License 12 | */ 13 | interface SimpleTransformer 14 | { 15 | /** 16 | * Transform the data without serializing, using the given transformer. 17 | * 18 | * @param mixed $data 19 | * @param \Flugg\Responder\Transformers\Transformer|callable|string|null $transformer 20 | * @param string|null $resourceKey 21 | * @return \Flugg\Responder\TransformBuilder 22 | */ 23 | public function make($data = null, $transformer = null, ?string $resourceKey = null): TransformBuilder; 24 | } 25 | -------------------------------------------------------------------------------- /src/Contracts/TransformFactory.php: -------------------------------------------------------------------------------- 1 | 13 | * @license The MIT License 14 | */ 15 | interface TransformFactory 16 | { 17 | /** 18 | * Transform the given resource, and serialize the data with the given serializer. 19 | * 20 | * @param \League\Fractal\Resource\ResourceInterface $resource 21 | * @param \League\Fractal\Serializer\SerializerAbstract $serializer 22 | * @param array $options 23 | * @return array|null 24 | */ 25 | public function make(ResourceInterface $resource, SerializerAbstract $serializer, array $options = []); 26 | } -------------------------------------------------------------------------------- /src/Contracts/Transformable.php: -------------------------------------------------------------------------------- 1 | 10 | * @license The MIT License 11 | */ 12 | interface Transformable 13 | { 14 | /** 15 | * Get a transformer for the class. 16 | * 17 | * @return \Flugg\Responder\Transformers\Transformer|callable|string|null 18 | */ 19 | public function transformer(); 20 | } -------------------------------------------------------------------------------- /src/Contracts/Transformers/TransformerResolver.php: -------------------------------------------------------------------------------- 1 | 10 | * @license The MIT License 11 | */ 12 | interface TransformerResolver 13 | { 14 | /** 15 | * Register a transformable to transformer binding. 16 | * 17 | * @param string|array $transformable 18 | * @param string|callback $transformer 19 | * @return void 20 | */ 21 | public function bind($transformable, $transformer); 22 | 23 | /** 24 | * Resolve a transformer. 25 | * 26 | * @param \Flugg\Responder\Transformers\Transformer|string|callable $transformer 27 | * @return \Flugg\Responder\Transformers\Transformer|callable 28 | * @throws \Flugg\Responder\Exceptions\InvalidTransformerException 29 | */ 30 | public function resolve($transformer); 31 | 32 | /** 33 | * Resolve a transformer from the given data. 34 | * 35 | * @param mixed $data 36 | * @return \Flugg\Responder\Transformers\Transformer|callable 37 | */ 38 | public function resolveFromData($data); 39 | } -------------------------------------------------------------------------------- /src/ErrorFactory.php: -------------------------------------------------------------------------------- 1 | 13 | * @license The MIT License 14 | */ 15 | class ErrorFactory implements ErrorFactoryContract 16 | { 17 | /** 18 | * A resolver for resolving messages from error codes. 19 | * 20 | * @var \Flugg\Responder\Contracts\ErrorMessageResolver 21 | */ 22 | protected $messageResolver; 23 | 24 | /** 25 | * Construct the factory class. 26 | * 27 | * @param \Flugg\Responder\Contracts\ErrorMessageResolver $messageResolver 28 | */ 29 | public function __construct(ErrorMessageResolverContract $messageResolver) 30 | { 31 | $this->messageResolver = $messageResolver; 32 | } 33 | 34 | /** 35 | * Make an error array from the given error code and message. 36 | * 37 | * @param \Flugg\Responder\Contracts\ErrorSerializer $serializer 38 | * @param mixed|null $errorCode 39 | * @param string|null $message 40 | * @param array|null $data 41 | * @return array 42 | */ 43 | public function make(ErrorSerializer $serializer, $errorCode = null, ?string $message = null, ?array $data = null): array 44 | { 45 | if (isset($errorCode) && ! isset($message)) { 46 | $message = $this->messageResolver->resolve($errorCode); 47 | } 48 | 49 | return $serializer->format($errorCode, $message, $data); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/ErrorMessageResolver.php: -------------------------------------------------------------------------------- 1 | 13 | * @license The MIT License 14 | */ 15 | class ErrorMessageResolver implements ErrorMessageResolverContract 16 | { 17 | /** 18 | * A serivce for resolving messages from language files. 19 | * 20 | * @var \Illuminate\Translation\Translator 21 | */ 22 | protected $translator; 23 | 24 | /** 25 | * A list of registered messages mapped to error codes. 26 | * 27 | * @var array 28 | */ 29 | protected $messages = []; 30 | 31 | /** 32 | * Construct the resolver class. 33 | * 34 | * @param \Illuminate\Translation\Translator $translator 35 | */ 36 | public function __construct(Translator $translator) 37 | { 38 | $this->translator = $translator; 39 | } 40 | 41 | /** 42 | * Register a message mapped to an error code. 43 | * 44 | * @param mixed $errorCode 45 | * @param string $message 46 | * @return void 47 | */ 48 | public function register($errorCode, string $message) 49 | { 50 | $this->messages = array_merge($this->messages, is_array($errorCode) ? $errorCode : [ 51 | $errorCode => $message, 52 | ]); 53 | } 54 | 55 | /** 56 | * Resolve a message from the given error code. 57 | * 58 | * @param mixed $errorCode 59 | * @return string|null 60 | */ 61 | public function resolve($errorCode) 62 | { 63 | if (key_exists($errorCode, $this->messages)) { 64 | return $this->messages[$errorCode]; 65 | } 66 | 67 | if ($this->translator->has($errorCode = "errors.$errorCode")) { 68 | return $this->translator->get($errorCode); 69 | } 70 | 71 | return null; 72 | } 73 | } -------------------------------------------------------------------------------- /src/Exceptions/ConvertsExceptions.php: -------------------------------------------------------------------------------- 1 | 26 | * @license The MIT License 27 | */ 28 | trait ConvertsExceptions 29 | { 30 | /** 31 | * A list of default exception types that should not be converted. 32 | * 33 | * @var array 34 | */ 35 | protected $dontConvert = []; 36 | 37 | /** 38 | * Convert an exception to another exception 39 | * 40 | * @param \Exception|\Throwable $exception 41 | * @param array $convert 42 | * @return void 43 | */ 44 | protected function convert($exception, array $convert) 45 | { 46 | foreach ($convert as $source => $target) { 47 | if ($exception instanceof $source) { 48 | if (is_callable($target)) { 49 | $target($exception); 50 | } 51 | 52 | throw new $target; 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * Convert a default exception to an API exception. 59 | * 60 | * @param \Exception|\Throwable $exception 61 | * @return void 62 | */ 63 | protected function convertDefaultException($exception) 64 | { 65 | $this->convert($exception, array_diff_key([ 66 | AuthenticationException::class => UnauthenticatedException::class, 67 | AuthorizationException::class => UnauthorizedException::class, 68 | NotFoundHttpException::class => PageNotFoundException::class, 69 | ModelNotFoundException::class => PageNotFoundException::class, 70 | BaseRelationNotFoundException::class => RelationNotFoundException::class, 71 | ValidationException::class => function ($exception) { 72 | throw new ValidationFailedException($exception->validator); 73 | }, 74 | ], array_flip($this->dontConvert))); 75 | } 76 | 77 | /** 78 | * Render an error response from an API exception. 79 | * 80 | * @param \Flugg\Responder\Exceptions\Http\HttpException $exception 81 | * @return \Illuminate\Http\JsonResponse 82 | */ 83 | protected function renderResponse(HttpException $exception): JsonResponse 84 | { 85 | return app(Responder::class) 86 | ->error($exception->errorCode(), $exception->message()) 87 | ->data($exception->data()) 88 | ->respond($exception->statusCode(), $exception->getHeaders()); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | 14 | * @license The MIT License 15 | */ 16 | class Handler extends ExceptionHandler 17 | { 18 | use ConvertsExceptions; 19 | 20 | /** 21 | * Render an exception into an HTTP response. 22 | * 23 | * @param \Illuminate\Http\Request $request 24 | * @param \Exception|\Throwable $exception 25 | * @return \Symfony\Component\HttpFoundation\Response 26 | */ 27 | public function render($request, $exception) 28 | { 29 | if ($request->wantsJson()) { 30 | $this->convertDefaultException($exception); 31 | 32 | if ($exception instanceof HttpException) { 33 | return $this->renderResponse($exception); 34 | } 35 | } 36 | 37 | return parent::render($request, $exception); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Exceptions/Http/HttpException.php: -------------------------------------------------------------------------------- 1 | 11 | * @license The MIT License 12 | */ 13 | abstract class HttpException extends BaseHttpException 14 | { 15 | /** 16 | * An HTTP status code. 17 | * 18 | * @var int 19 | */ 20 | protected $status = 500; 21 | 22 | /** 23 | * An error code. 24 | * 25 | * @var string|null 26 | */ 27 | protected $errorCode = null; 28 | 29 | /** 30 | * An error message. 31 | * 32 | * @var string 33 | */ 34 | protected $message = ''; 35 | 36 | /** 37 | * Additional error data. 38 | * 39 | * @var array|null 40 | */ 41 | protected $data = null; 42 | 43 | /** 44 | * Attached headers. 45 | * 46 | * @var array 47 | */ 48 | protected $headers = []; 49 | 50 | /** 51 | * Construct the exception class. 52 | * 53 | * @param string|null $message 54 | * @param array|null $headers 55 | */ 56 | public function __construct(?string $message = null, ?array $headers = null) 57 | { 58 | parent::__construct($this->status, $message ?? $this->message, null, $headers ?? $this->headers); 59 | } 60 | 61 | /** 62 | * Retrieve the HTTP status code,. 63 | * 64 | * @return int 65 | */ 66 | public function statusCode(): int 67 | { 68 | return $this->status; 69 | } 70 | 71 | /** 72 | * Retrieve the error code. 73 | * 74 | * @return string|null 75 | */ 76 | public function errorCode() 77 | { 78 | return $this->errorCode; 79 | } 80 | 81 | /** 82 | * Retrieve the error message. 83 | * 84 | * @return string|null 85 | */ 86 | public function message() 87 | { 88 | return $this->message ?: null; 89 | } 90 | 91 | /** 92 | * Retrieve additional error data. 93 | * 94 | * @return array|null 95 | */ 96 | public function data() 97 | { 98 | return $this->data; 99 | } 100 | 101 | /** 102 | * Retrieve attached headers. 103 | * 104 | * @return array|null 105 | */ 106 | public function headers() 107 | { 108 | return $this->headers; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Exceptions/Http/PageNotFoundException.php: -------------------------------------------------------------------------------- 1 | 10 | * @license The MIT License 11 | */ 12 | class PageNotFoundException extends HttpException 13 | { 14 | /** 15 | * An HTTP status code. 16 | * 17 | * @var int 18 | */ 19 | protected $status = 404; 20 | 21 | /** 22 | * An error code. 23 | * 24 | * @var string|null 25 | */ 26 | protected $errorCode = 'page_not_found'; 27 | } -------------------------------------------------------------------------------- /src/Exceptions/Http/RelationNotFoundException.php: -------------------------------------------------------------------------------- 1 | 10 | * @license The MIT License 11 | */ 12 | class RelationNotFoundException extends HttpException 13 | { 14 | /** 15 | * An HTTP status code. 16 | * 17 | * @var int 18 | */ 19 | protected $status = 422; 20 | 21 | /** 22 | * An error code. 23 | * 24 | * @var string|null 25 | */ 26 | protected $errorCode = 'relation_not_found'; 27 | } -------------------------------------------------------------------------------- /src/Exceptions/Http/UnauthenticatedException.php: -------------------------------------------------------------------------------- 1 | 11 | * @license The MIT License 12 | */ 13 | class UnauthenticatedException extends HttpException 14 | { 15 | /** 16 | * An HTTP status code. 17 | * 18 | * @var int 19 | */ 20 | protected $status = 401; 21 | 22 | /** 23 | * The error code. 24 | * 25 | * @var string|null 26 | */ 27 | protected $errorCode = 'unauthenticated'; 28 | } -------------------------------------------------------------------------------- /src/Exceptions/Http/UnauthorizedException.php: -------------------------------------------------------------------------------- 1 | 11 | * @license The MIT License 12 | */ 13 | class UnauthorizedException extends HttpException 14 | { 15 | /** 16 | * An HTTP status code. 17 | * 18 | * @var int 19 | */ 20 | protected $status = 401; 21 | 22 | /** 23 | * An error code. 24 | * 25 | * @var string|null 26 | */ 27 | protected $errorCode = 'unauthorized'; 28 | } 29 | -------------------------------------------------------------------------------- /src/Exceptions/Http/ValidationFailedException.php: -------------------------------------------------------------------------------- 1 | 13 | * @license The MIT License 14 | */ 15 | class ValidationFailedException extends HttpException 16 | { 17 | /** 18 | * An HTTP status code. 19 | * 20 | * @var int 21 | */ 22 | protected $status = 422; 23 | 24 | /** 25 | * An error code. 26 | * 27 | * @var string|null 28 | */ 29 | protected $errorCode = 'validation_failed'; 30 | 31 | /** 32 | * A validator for fetching validation messages. 33 | * 34 | * @var \Illuminate\Contracts\Validation\Validator 35 | */ 36 | protected $validator; 37 | 38 | /** 39 | * Construct the exception class. 40 | * 41 | * @param \Illuminate\Contracts\Validation\Validator $validator 42 | */ 43 | public function __construct(Validator $validator) 44 | { 45 | $this->validator = $validator; 46 | 47 | parent::__construct(); 48 | } 49 | 50 | /** 51 | * Retrieve the error data. 52 | * 53 | * @return array|null 54 | */ 55 | public function data() 56 | { 57 | return ['fields' => $this->validator->getMessageBag()->toArray()]; 58 | } 59 | } -------------------------------------------------------------------------------- /src/Exceptions/InvalidErrorSerializerException.php: -------------------------------------------------------------------------------- 1 | 13 | * @license The MIT License 14 | */ 15 | class InvalidErrorSerializerException extends RuntimeException 16 | { 17 | /** 18 | * Construct the exception class. 19 | */ 20 | public function __construct() 21 | { 22 | parent::__construct('Serializer must be an instance of [' . ErrorSerializer::class . '].'); 23 | } 24 | } -------------------------------------------------------------------------------- /src/Exceptions/InvalidSuccessSerializerException.php: -------------------------------------------------------------------------------- 1 | 13 | * @license The MIT License 14 | */ 15 | class InvalidSuccessSerializerException extends RuntimeException 16 | { 17 | /** 18 | * Construct the exception class. 19 | */ 20 | public function __construct() 21 | { 22 | parent::__construct('Serializer must be an instance of [' . SerializerAbstract::class . '].'); 23 | } 24 | } -------------------------------------------------------------------------------- /src/Exceptions/InvalidTransformerException.php: -------------------------------------------------------------------------------- 1 | 13 | * @license The MIT License 14 | */ 15 | class InvalidTransformerException extends RuntimeException 16 | { 17 | /** 18 | * Construct the exception class. 19 | */ 20 | public function __construct() 21 | { 22 | parent::__construct('Transformer must be a callable or an instance of [' . Transformer::class . '].'); 23 | } 24 | } -------------------------------------------------------------------------------- /src/Facades/Responder.php: -------------------------------------------------------------------------------- 1 | 13 | * @license The MIT License 14 | * 15 | * @see \Flugg\Responder\Responder 16 | */ 17 | class Responder extends Facade 18 | { 19 | /** 20 | * Get the registered name of the component. 21 | * 22 | * @return string 23 | */ 24 | protected static function getFacadeAccessor() 25 | { 26 | return ResponderContract::class; 27 | } 28 | } -------------------------------------------------------------------------------- /src/Facades/Transformation.php: -------------------------------------------------------------------------------- 1 | 13 | * @license The MIT License 14 | * 15 | * @see \Flugg\Responder\Transformer 16 | */ 17 | class Transformation extends Facade 18 | { 19 | /** 20 | * Get the registered name of the component. 21 | * 22 | * @return string 23 | */ 24 | protected static function getFacadeAccessor() 25 | { 26 | return TransformationService::class; 27 | } 28 | } -------------------------------------------------------------------------------- /src/FractalTransformFactory.php: -------------------------------------------------------------------------------- 1 | 16 | * @license The MIT License 17 | */ 18 | class FractalTransformFactory implements TransformFactory 19 | { 20 | /** 21 | * A manager for executing transforms. 22 | * 23 | * @var \League\Fractal\Manager 24 | */ 25 | protected $manager; 26 | 27 | /** 28 | * Construct the factory class. 29 | * 30 | * @param \League\Fractal\Manager $manager 31 | */ 32 | public function __construct(Manager $manager) 33 | { 34 | $this->manager = $manager; 35 | } 36 | 37 | /** 38 | * Transform the given resource, and serialize the data with the given serializer. 39 | * 40 | * @param \League\Fractal\Resource\ResourceInterface $resource 41 | * @param \League\Fractal\Serializer\SerializerAbstract $serializer 42 | * @param array $options 43 | * @return array|null 44 | */ 45 | public function make(ResourceInterface $resource, SerializerAbstract $serializer, array $options = []) 46 | { 47 | $options = $this->parseOptions($options, $resource); 48 | 49 | return $this->manager->setSerializer($serializer) 50 | ->parseIncludes($options['includes']) 51 | ->parseExcludes($options['excludes']) 52 | ->parseFieldsets($options['fieldsets']) 53 | ->createData($resource) 54 | ->toArray(); 55 | } 56 | 57 | /** 58 | * Parse the transformation options. 59 | * 60 | * @param array $options 61 | * @param \League\Fractal\Resource\ResourceInterface $resource 62 | * @return array 63 | */ 64 | protected function parseOptions(array $options, ResourceInterface $resource): array 65 | { 66 | $options = array_merge([ 67 | 'includes' => [], 68 | 'excludes' => [], 69 | 'fieldsets' => [], 70 | ], $options); 71 | 72 | if (! empty($options['fieldsets'])) { 73 | if (empty($resourceKey = $resource->getResourceKey())) { 74 | throw new LogicException('Filtering fields using sparse fieldsets require resource key to be set.'); 75 | } 76 | 77 | $options['fieldsets'] = $this->parseFieldsets($options['fieldsets'], $resourceKey, $options['includes']); 78 | } 79 | 80 | return $options; 81 | } 82 | 83 | /** 84 | * Parse the fieldsets for Fractal. 85 | * 86 | * @param array $fieldsets 87 | * @param string $resourceKey 88 | * @param array $includes 89 | * @return array 90 | */ 91 | protected function parseFieldsets(array $fieldsets, string $resourceKey, array $includes): array 92 | { 93 | $includes = array_map(function ($include) use ($resourceKey) { 94 | return "$resourceKey.$include"; 95 | }, $includes); 96 | 97 | foreach ($fieldsets as $key => $fields) { 98 | if (is_numeric($key)) { 99 | unset($fieldsets[$key]); 100 | $key = $resourceKey; 101 | } 102 | 103 | $fields = $this->parseFieldset($key, (array) $fields, $includes); 104 | $fieldsets[$key] = array_unique(array_merge(key_exists($key, $fieldsets) ? (array) $fieldsets[$key] : [], $fields)); 105 | } 106 | 107 | return array_map(function ($fields) { 108 | return implode(',', $fields); 109 | }, $fieldsets); 110 | } 111 | 112 | /** 113 | * Parse the given fieldset and append any related resource keys. 114 | * 115 | * @param string $key 116 | * @param array $fields 117 | * @param array $includes 118 | * @return array 119 | */ 120 | protected function parseFieldset(string $key, array $fields, array $includes): array 121 | { 122 | $childIncludes = array_reduce($includes, function ($segments, $include) use ($key) { 123 | return array_merge($segments, $this->resolveChildIncludes($key, $include)); 124 | }, []); 125 | 126 | return array_merge($fields, array_unique($childIncludes)); 127 | } 128 | 129 | /** 130 | * Resolve included segments that are a direct child to the given resource key. 131 | * 132 | * @param string $key 133 | * @param string $include 134 | * @return array 135 | */ 136 | protected function resolveChildIncludes($key, string $include): array 137 | { 138 | if (count($segments = explode('.', $include)) <= 1) { 139 | return []; 140 | } 141 | 142 | $relation = $key === array_shift($segments) ? [$segments[0]] : []; 143 | 144 | return array_merge($relation, $this->resolveChildIncludes($key, implode('.', $segments))); 145 | } 146 | } -------------------------------------------------------------------------------- /src/Http/MakesResponses.php: -------------------------------------------------------------------------------- 1 | 13 | * @license The MIT License 14 | */ 15 | trait MakesResponses 16 | { 17 | /** 18 | * Build a successful response. 19 | * 20 | * @param mixed $data 21 | * @param callable|string|\Flugg\Responder\Transformers\Transformer|null $transformer 22 | * @param string|null $resourceKey 23 | * @return \Flugg\Responder\Http\Responses\SuccessResponseBuilder 24 | */ 25 | public function success($data = null, $transformer = null, ?string $resourceKey = null): SuccessResponseBuilder 26 | { 27 | return app(Responder::class)->success(...func_get_args()); 28 | } 29 | 30 | /** 31 | * Build an error response. 32 | * 33 | * @param mixed|null $errorCode 34 | * @param string|null $message 35 | * @return \Flugg\Responder\Http\Responses\ErrorResponseBuilder 36 | */ 37 | public function error($errorCode = null, ?string $message = null): ErrorResponseBuilder 38 | { 39 | return app(Responder::class)->error(...func_get_args()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Http/Middleware/ConvertToSnakeCase.php: -------------------------------------------------------------------------------- 1 | 13 | * @license The MIT License 14 | */ 15 | class ConvertToSnakeCase extends TransformsRequest 16 | { 17 | /** 18 | * A list of attributes that shouldn't be converted. 19 | * 20 | * @var array 21 | */ 22 | protected $except = [ 23 | // 24 | ]; 25 | 26 | /** 27 | * Clean the data in the given array. 28 | * 29 | * @param array $data 30 | * @param string $keyPrefix 31 | * @return array 32 | */ 33 | protected function cleanArray(array $data, $keyPrefix = '') 34 | { 35 | $parameters = []; 36 | 37 | foreach ($data as $key => $value) { 38 | $parameters[in_array($keyPrefix.$key, $this->except) ? $keyPrefix.$key : Str::snake($keyPrefix.$key)] = $value; 39 | } 40 | 41 | return $parameters; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Http/Responses/Decorators/EscapeHtmlDecorator.php: -------------------------------------------------------------------------------- 1 | 12 | * @license The MIT License 13 | */ 14 | class EscapeHtmlDecorator extends ResponseDecorator 15 | { 16 | /** 17 | * Generate a JSON response. 18 | * 19 | * @param array $data 20 | * @param int $status 21 | * @param array $headers 22 | * @return \Illuminate\Http\JsonResponse 23 | */ 24 | public function make(array $data, int $status, array $headers = []): JsonResponse 25 | { 26 | array_walk_recursive($data, function (&$value) { 27 | if (is_string($value)) { 28 | $value = e($value); 29 | } 30 | }); 31 | 32 | return $this->factory->make($data, $status, $headers); 33 | } 34 | } -------------------------------------------------------------------------------- /src/Http/Responses/Decorators/PrettyPrintDecorator.php: -------------------------------------------------------------------------------- 1 | 12 | * @license The MIT License 13 | */ 14 | class PrettyPrintDecorator extends ResponseDecorator 15 | { 16 | /** 17 | * Generate a JSON response. 18 | * 19 | * @param array $data 20 | * @param int $status 21 | * @param array $headers 22 | * @return \Illuminate\Http\JsonResponse 23 | */ 24 | public function make(array $data, int $status, array $headers = []): JsonResponse 25 | { 26 | $response = $this->factory->make($data, $status, $headers); 27 | 28 | $response->setEncodingOptions($response->getEncodingOptions() | JSON_PRETTY_PRINT); 29 | 30 | return $response; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Http/Responses/Decorators/ResponseDecorator.php: -------------------------------------------------------------------------------- 1 | 13 | * @license The MIT License 14 | */ 15 | abstract class ResponseDecorator implements ResponseFactory 16 | { 17 | /** 18 | * The factory being decorated. 19 | * 20 | * @var \Flugg\Responder\Contracts\ResponseFactory 21 | */ 22 | protected $factory; 23 | 24 | /** 25 | * Construct the decorator class. 26 | * 27 | * @param \Flugg\Responder\Contracts\ResponseFactory $factory 28 | */ 29 | public function __construct(ResponseFactory $factory) 30 | { 31 | $this->factory = $factory; 32 | } 33 | 34 | /** 35 | * Generate a JSON response. 36 | * 37 | * @param array $data 38 | * @param int $status 39 | * @param array $headers 40 | * @return \Illuminate\Http\JsonResponse 41 | */ 42 | abstract public function make(array $data, int $status, array $headers = []): JsonResponse; 43 | } 44 | -------------------------------------------------------------------------------- /src/Http/Responses/Decorators/StatusCodeDecorator.php: -------------------------------------------------------------------------------- 1 | 12 | * @license The MIT License 13 | */ 14 | class StatusCodeDecorator extends ResponseDecorator 15 | { 16 | /** 17 | * Generate a JSON response. 18 | * 19 | * @param array $data 20 | * @param int $status 21 | * @param array $headers 22 | * @return \Illuminate\Http\JsonResponse 23 | */ 24 | public function make(array $data, int $status, array $headers = []): JsonResponse 25 | { 26 | return $this->factory->make(array_merge([ 27 | 'status' => $status, 28 | ], $data), $status, $headers); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Http/Responses/Decorators/SuccessFlagDecorator.php: -------------------------------------------------------------------------------- 1 | 12 | * @license The MIT License 13 | */ 14 | class SuccessFlagDecorator extends ResponseDecorator 15 | { 16 | /** 17 | * Generate a JSON response. 18 | * 19 | * @param array $data 20 | * @param int $status 21 | * @param array $headers 22 | * @return \Illuminate\Http\JsonResponse 23 | */ 24 | public function make(array $data, int $status, array $headers = []): JsonResponse 25 | { 26 | return $this->factory->make(array_merge([ 27 | 'success' => $status >= 100 && $status < 400, 28 | ], $data), $status, $headers); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Http/Responses/ErrorResponseBuilder.php: -------------------------------------------------------------------------------- 1 | 15 | * @license The MIT License 16 | */ 17 | class ErrorResponseBuilder extends ResponseBuilder 18 | { 19 | /** 20 | * A factory for building error data output. 21 | * 22 | * @var \Flugg\Responder\Contracts\ErrorFactory 23 | */ 24 | private $errorFactory; 25 | 26 | /** 27 | * A serializer for formatting error data. 28 | * 29 | * @var \Flugg\Responder\Contracts\ErrorSerializer 30 | */ 31 | protected $serializer; 32 | 33 | /** 34 | * A code representing the error. 35 | * 36 | * @var string|null 37 | */ 38 | protected $errorCode = null; 39 | 40 | /** 41 | * A message descibing the error. 42 | * 43 | * @var string|null 44 | */ 45 | protected $message = null; 46 | 47 | /** 48 | * Additional data included with the error. 49 | * 50 | * @var array|null 51 | */ 52 | protected $data = null; 53 | 54 | /** 55 | * A HTTP status code for the response. 56 | * 57 | * @var int 58 | */ 59 | protected $status = 500; 60 | 61 | /** 62 | * Construct the builder class. 63 | * 64 | * @param \Flugg\Responder\Contracts\ResponseFactory $responseFactory 65 | * @param \Flugg\Responder\Contracts\ErrorFactory $errorFactory 66 | */ 67 | public function __construct(ResponseFactory $responseFactory, ErrorFactory $errorFactory) 68 | { 69 | $this->errorFactory = $errorFactory; 70 | 71 | parent::__construct($responseFactory); 72 | } 73 | 74 | /** 75 | * Set the error code and message. 76 | * 77 | * @param mixed|null $errorCode 78 | * @param string|null $message 79 | * @return $this 80 | */ 81 | public function error($errorCode = null, ?string $message = null) 82 | { 83 | $this->errorCode = $errorCode; 84 | $this->message = $message; 85 | 86 | return $this; 87 | } 88 | 89 | /** 90 | * Add additional data to the error. 91 | * 92 | * @param array|null $data 93 | * @return $this 94 | */ 95 | public function data(?array $data = null) 96 | { 97 | $this->data = array_merge((array) $this->data, (array) $data); 98 | 99 | return $this; 100 | } 101 | 102 | /** 103 | * Set the error serializer. 104 | * 105 | * @param \Flugg\Responder\Contracts\ErrorSerializer|string $serializer 106 | * @return $this 107 | * 108 | * @throws \Flugg\Responder\Exceptions\InvalidErrorSerializerException 109 | */ 110 | public function serializer($serializer) 111 | { 112 | if (is_string($serializer)) { 113 | $serializer = new $serializer; 114 | } 115 | 116 | if (! $serializer instanceof ErrorSerializer) { 117 | throw new InvalidErrorSerializerException; 118 | } 119 | 120 | $this->serializer = $serializer; 121 | 122 | return $this; 123 | } 124 | 125 | /** 126 | * Get the serialized response output. 127 | * 128 | * @return array 129 | */ 130 | protected function getOutput(): array 131 | { 132 | return $this->errorFactory->make($this->serializer, $this->errorCode, $this->message, $this->data); 133 | } 134 | 135 | /** 136 | * Validate the HTTP status code for the response. 137 | * 138 | * @param int $status 139 | * @return void 140 | * 141 | * @throws \InvalidArgumentException 142 | */ 143 | protected function validateStatusCode(int $status) 144 | { 145 | if ($status < 400 || $status >= 600) { 146 | throw new InvalidArgumentException("{$status} is not a valid error HTTP status code."); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Http/Responses/Factories/LaravelResponseFactory.php: -------------------------------------------------------------------------------- 1 | 14 | * @license The MIT License 15 | */ 16 | class LaravelResponseFactory implements ResponseFactory 17 | { 18 | /** 19 | * The Laravel factory for making responses. 20 | * 21 | * @var \Illuminate\Contracts\Routing\ResponseFactory 22 | */ 23 | protected $factory; 24 | 25 | /** 26 | * Construct the factory class. 27 | * 28 | * @param \Illuminate\Contracts\Routing\ResponseFactory $factory 29 | */ 30 | public function __construct(BaseLaravelResponseFactory $factory) 31 | { 32 | $this->factory = $factory; 33 | } 34 | 35 | /** 36 | * Generate a JSON response. 37 | * 38 | * @param array $data 39 | * @param int $status 40 | * @param array $headers 41 | * @return \Illuminate\Http\JsonResponse 42 | */ 43 | public function make(array $data, int $status, array $headers = []): JsonResponse 44 | { 45 | return $this->factory->json($data, $status, $headers); 46 | } 47 | } -------------------------------------------------------------------------------- /src/Http/Responses/Factories/LumenResponseFactory.php: -------------------------------------------------------------------------------- 1 | 14 | * @license The MIT License 15 | */ 16 | class LumenResponseFactory implements ResponseFactory 17 | { 18 | /** 19 | * The Lumen factory for making responses. 20 | * 21 | * @var \Laravel\Lumen\Http\ResponseFactory 22 | */ 23 | protected $factory; 24 | 25 | /** 26 | * Construct the factory class. 27 | * 28 | * @param \Laravel\Lumen\Http\ResponseFactory $factory 29 | */ 30 | public function __construct(BaseLumenResponseFactory $factory) 31 | { 32 | $this->factory = $factory; 33 | } 34 | 35 | /** 36 | * Generate a JSON response. 37 | * 38 | * @param array $data 39 | * @param int $status 40 | * @param array $headers 41 | * @return \Illuminate\Http\JsonResponse 42 | */ 43 | public function make(array $data, int $status, array $headers = []): JsonResponse 44 | { 45 | return $this->factory->json($data, $status, $headers); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Http/Responses/ResponseBuilder.php: -------------------------------------------------------------------------------- 1 | 15 | * @license The MIT License 16 | */ 17 | abstract class ResponseBuilder implements Arrayable, Jsonable 18 | { 19 | /** 20 | * A factory for making responses. 21 | * 22 | * @var \Flugg\Responder\Contracts\ResponseFactory 23 | */ 24 | protected $responseFactory; 25 | 26 | /** 27 | * A HTTP status code for the response. 28 | * 29 | * @var int 30 | */ 31 | protected $status; 32 | 33 | /** 34 | * Construct the builder class. 35 | * 36 | * @param \Flugg\Responder\Contracts\ResponseFactory $responseFactory 37 | */ 38 | public function __construct(ResponseFactory $responseFactory) 39 | { 40 | $this->responseFactory = $responseFactory; 41 | } 42 | 43 | /** 44 | * Decorate the response with the given decorator. 45 | * 46 | * @param string[]|string $decorator 47 | * @return $this 48 | */ 49 | public function decorator($decorator) 50 | { 51 | $decorators = is_array($decorator) ? $decorator : func_get_args(); 52 | 53 | foreach ($decorators as $decorator) { 54 | $this->responseFactory = new $decorator($this->responseFactory); 55 | } 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * Respond with an HTTP response. 62 | * 63 | * @param int|null $status 64 | * @param array $headers 65 | * @return \Illuminate\Http\JsonResponse 66 | */ 67 | public function respond(?int $status = null, array $headers = []): JsonResponse 68 | { 69 | if (! is_null($status)) { 70 | $this->setStatusCode($status); 71 | } 72 | 73 | return $this->responseFactory->make($this->getOutput(), $this->status, $headers); 74 | } 75 | 76 | /** 77 | * Convert the response to an array. 78 | * 79 | * @return array 80 | */ 81 | public function toArray(): array 82 | { 83 | return $this->respond()->getData(true); 84 | } 85 | 86 | /** 87 | * Convert the response to an Illuminate collection. 88 | * 89 | * @return \Illuminate\Support\Collection 90 | */ 91 | public function toCollection(): Collection 92 | { 93 | return new Collection($this->toArray()); 94 | } 95 | 96 | /** 97 | * Convert the response to JSON. 98 | * 99 | * @param int $options 100 | * @return string 101 | */ 102 | public function toJson($options = 0): string 103 | { 104 | return json_encode($this->toArray(), $options); 105 | } 106 | 107 | /** 108 | * Set the HTTP status code for the response. 109 | * 110 | * @param int $status 111 | * @return void 112 | */ 113 | protected function setStatusCode(int $status) 114 | { 115 | $this->validateStatusCode($this->status = $status); 116 | } 117 | 118 | /** 119 | * Get the serialized response output. 120 | * 121 | * @return array 122 | */ 123 | abstract protected function getOutput(): array; 124 | 125 | /** 126 | * Convert the response to an array. 127 | * 128 | * @param int $status 129 | * @return void 130 | */ 131 | abstract protected function validateStatusCode(int $status); 132 | } 133 | -------------------------------------------------------------------------------- /src/Http/Responses/SuccessResponseBuilder.php: -------------------------------------------------------------------------------- 1 | 17 | * @license The MIT License 18 | * 19 | * @method $this meta(array $meta) 20 | * @method $this with(array | string $relations) 21 | * @method $this without(array | string $relations) 22 | * @method $this serializer(SerializerAbstract | string $serializer) 23 | * @method $this paginator(IlluminatePaginatorAdapter $paginator) 24 | * @method $this cursor(Cursor $cursor) 25 | */ 26 | class SuccessResponseBuilder extends ResponseBuilder 27 | { 28 | /** 29 | * A builder for building transformed arrays. 30 | * 31 | * @var \Flugg\Responder\TransformBuilder 32 | */ 33 | protected $transformBuilder; 34 | 35 | /** 36 | * A HTTP status code for the response. 37 | * 38 | * @var int 39 | */ 40 | protected $status = 200; 41 | 42 | /** 43 | * Construct the builder class. 44 | * 45 | * @param \Flugg\Responder\Contracts\ResponseFactory $responseFactory 46 | * @param \Flugg\Responder\TransformBuilder $transformBuilder 47 | */ 48 | public function __construct(ResponseFactory $responseFactory, TransformBuilder $transformBuilder) 49 | { 50 | $this->transformBuilder = $transformBuilder; 51 | 52 | parent::__construct($responseFactory); 53 | } 54 | 55 | /** 56 | * Set resource data for the transformation. 57 | * 58 | * @param mixed $data 59 | * @param \Flugg\Responder\Transformers\Transformer|callable|string|null $transformer 60 | * @param string|null $resourceKey 61 | * @return self 62 | */ 63 | public function transform($data = null, $transformer = null, ?string $resourceKey = null): SuccessResponseBuilder 64 | { 65 | $this->transformBuilder->resource($data, $transformer, $resourceKey); 66 | 67 | return $this; 68 | } 69 | 70 | /** 71 | * Dynamically send calls to the transform builder. 72 | * 73 | * @param string $name 74 | * @param array $arguments 75 | * @return self|void 76 | */ 77 | public function __call($name, $arguments) 78 | { 79 | if (in_array($name, ['cursor', 'paginator', 'meta', 'with', 'without', 'only', 'serializer'])) { 80 | $this->transformBuilder->$name(...$arguments); 81 | 82 | return $this; 83 | } 84 | 85 | throw new BadMethodCallException; 86 | } 87 | 88 | /** 89 | * Get the serialized response output. 90 | * 91 | * @return mixed 92 | */ 93 | protected function getOutput(): array 94 | { 95 | return $this->transformBuilder->transform(); 96 | } 97 | 98 | /** 99 | * Validate the HTTP status code for the response. 100 | * 101 | * @param int $status 102 | * @return void 103 | * 104 | * @throws \InvalidArgumentException 105 | */ 106 | protected function validateStatusCode(int $status) 107 | { 108 | if ($status < 100 || $status >= 400) { 109 | throw new InvalidArgumentException("{$status} is not a valid success HTTP status code."); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Pagination/CursorPaginator.php: -------------------------------------------------------------------------------- 1 | 14 | * @license The MIT License 15 | */ 16 | class CursorPaginator 17 | { 18 | /** 19 | * A list of the items being paginated. 20 | * 21 | * @var \Illuminate\Support\Collection 22 | */ 23 | protected $items; 24 | 25 | /** 26 | * The current cursor reference. 27 | * 28 | * @var int|string|null 29 | */ 30 | protected $cursor; 31 | 32 | /** 33 | * The previous cursor reference. 34 | * 35 | * @var int|string|null 36 | */ 37 | protected $previousCursor; 38 | 39 | /** 40 | * The next cursor reference. 41 | * 42 | * @var int|string|null 43 | */ 44 | protected $nextCursor; 45 | 46 | /** 47 | * The current cursor resolver callback. 48 | * 49 | * @var \Closure|null 50 | */ 51 | protected static $currentCursorResolver; 52 | 53 | /** 54 | * Create a new paginator instance. 55 | * 56 | * @param \Illuminate\Support\Collection|array|null $data 57 | * @param int|string|null $cursor 58 | * @param int|string|null $previousCursor 59 | * @param int|string|null $nextCursor 60 | */ 61 | public function __construct($data, $cursor, $previousCursor, $nextCursor) 62 | { 63 | $this->cursor = $cursor; 64 | $this->previousCursor = $previousCursor; 65 | $this->nextCursor = $nextCursor; 66 | 67 | $this->set($data); 68 | } 69 | 70 | /** 71 | * Retrieve the current cursor reference. 72 | * 73 | * @return int|string|null 74 | */ 75 | public function cursor() 76 | { 77 | return $this->cursor; 78 | } 79 | 80 | /** 81 | * Retireve the next cursor reference. 82 | * 83 | * @return int|string|null 84 | */ 85 | public function previous() 86 | { 87 | return $this->previousCursor; 88 | } 89 | 90 | /** 91 | * Retireve the next cursor reference. 92 | * 93 | * @return int|string|null 94 | */ 95 | public function next() 96 | { 97 | return $this->nextCursor; 98 | } 99 | 100 | /** 101 | * Get the slice of items being paginated. 102 | * 103 | * @return array 104 | */ 105 | public function items(): array 106 | { 107 | return $this->items->all(); 108 | } 109 | 110 | /** 111 | * Get the paginator's underlying collection. 112 | * 113 | * @return \Illuminate\Support\Collection 114 | */ 115 | public function get(): Collection 116 | { 117 | return $this->items; 118 | } 119 | 120 | /** 121 | * Set the paginator's underlying collection. 122 | * 123 | * @param \Illuminate\Support\Collection|array|null $data 124 | * @return self 125 | */ 126 | public function set($data): CursorPaginator 127 | { 128 | $this->items = $data instanceof Collection ? $data : collect($data); 129 | 130 | return $this; 131 | } 132 | 133 | /** 134 | * Resolve the current cursor using the cursor resolver. 135 | * 136 | * @param string $name 137 | * @return mixed 138 | * @throws \LogicException 139 | */ 140 | public static function resolveCursor(string $name = 'cursor') 141 | { 142 | if (isset(static::$currentCursorResolver)) { 143 | return call_user_func(static::$currentCursorResolver, $name); 144 | } 145 | 146 | throw new LogicException("Could not resolve cursor with the name [{$name}]."); 147 | } 148 | 149 | /** 150 | * Set the current cursor resolver callback. 151 | * 152 | * @param \Closure $resolver 153 | * @return void 154 | */ 155 | public static function cursorResolver(Closure $resolver) 156 | { 157 | static::$currentCursorResolver = $resolver; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Pagination/PaginatorFactory.php: -------------------------------------------------------------------------------- 1 | 16 | * @license The MIT License 17 | */ 18 | class PaginatorFactory implements PaginatorFactoryContract 19 | { 20 | /** 21 | * A list of query string values appended to the paginator links. 22 | * 23 | * @var array 24 | */ 25 | protected $parameters; 26 | 27 | /** 28 | * Construct the factory class. 29 | * 30 | * @param array $parameters 31 | */ 32 | public function __construct(array $parameters) 33 | { 34 | $this->parameters = $parameters; 35 | } 36 | 37 | /** 38 | * Make a Fractal paginator adapter from a Laravel paginator. 39 | * 40 | * @param \Illuminate\Contracts\Pagination\LengthAwarePaginator $paginator 41 | * @return \League\Fractal\Pagination\PaginatorInterface 42 | */ 43 | public function make(LengthAwarePaginator $paginator): PaginatorInterface 44 | { 45 | $paginator->appends($this->parameters); 46 | 47 | return new IlluminatePaginatorAdapter($paginator); 48 | } 49 | 50 | /** 51 | * Make a Fractal paginator adapter from a Laravel paginator. 52 | * 53 | * @param \Flugg\Responder\Pagination\CursorPaginator $paginator 54 | * @return \League\Fractal\Pagination\Cursor 55 | */ 56 | public function makeCursor(CursorPaginator $paginator): Cursor 57 | { 58 | return new Cursor($paginator->cursor(), $paginator->previous(), $paginator->next(), $paginator->get()->count()); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Resources/DataNormalizer.php: -------------------------------------------------------------------------------- 1 | 20 | * @license The MIT License 21 | */ 22 | class DataNormalizer 23 | { 24 | /** 25 | * Normalize the data for a resource. 26 | * 27 | * @param mixed $data 28 | * @return mixed 29 | */ 30 | public function normalize($data = null) 31 | { 32 | if ($this->isInstanceOf($data, [Builder::class, EloquentBuilder::class, CursorPaginator::class])) { 33 | return $data->get(); 34 | } elseif ($data instanceof Paginator) { 35 | return $data->getCollection(); 36 | } elseif ($data instanceof Relation) { 37 | return $this->normalizeRelation($data); 38 | } 39 | 40 | return $data; 41 | } 42 | 43 | /** 44 | * Normalize a relationship. 45 | * 46 | * @param \Illuminate\Database\Eloquent\Relations\Relation $relation 47 | * @return \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|null 48 | */ 49 | protected function normalizeRelation(Relation $relation) 50 | { 51 | if ($this->isInstanceOf($relation, [BelongsTo::class, HasOne::class, MorphOne::class, MorphTo::class])) { 52 | return $relation->first(); 53 | } 54 | 55 | return $relation->get(); 56 | } 57 | 58 | /** 59 | * Indicates if the given data is an instance of any of the given class names. 60 | * 61 | * @param mixed $data 62 | * @param array $classes 63 | * @return bool 64 | */ 65 | protected function isInstanceOf($data, array $classes): bool 66 | { 67 | foreach ($classes as $class) { 68 | if ($data instanceof $class) { 69 | return true; 70 | } 71 | } 72 | 73 | return false; 74 | } 75 | } -------------------------------------------------------------------------------- /src/Resources/ResourceFactory.php: -------------------------------------------------------------------------------- 1 | 20 | * @license The MIT License 21 | */ 22 | class ResourceFactory implements ResourceFactoryContract 23 | { 24 | /** 25 | * A service class, used to normalize data. 26 | * 27 | * @var \Flugg\Responder\Resources\DataNormalizer 28 | */ 29 | protected $normalizer; 30 | 31 | /** 32 | * A resolver class, used to resolve resource keys. 33 | * 34 | * @var \Flugg\Responder\Contracts\Transformers\TransformerResolver 35 | */ 36 | protected $transformerResolver; 37 | 38 | /** 39 | * A resolver class, used to resolve resource keys. 40 | * 41 | * @var \Flugg\Responder\Contracts\Resources\ResourceKeyResolver 42 | */ 43 | protected $resourceKeyResolver; 44 | 45 | /** 46 | * Construct the factory class. 47 | * 48 | * @param \Flugg\Responder\Resources\DataNormalizer $normalizer 49 | * @param \Flugg\Responder\Contracts\Transformers\TransformerResolver $transformerResolver 50 | * @param \Flugg\Responder\Contracts\Resources\ResourceKeyResolver $resourceKeyResolver 51 | */ 52 | public function __construct(DataNormalizer $normalizer, TransformerResolver $transformerResolver, ResourceKeyResolverContract $resourceKeyResolver) 53 | { 54 | $this->normalizer = $normalizer; 55 | $this->transformerResolver = $transformerResolver; 56 | $this->resourceKeyResolver = $resourceKeyResolver; 57 | } 58 | 59 | /** 60 | * Make resource from the given data. 61 | * 62 | * @param mixed $data 63 | * @param \Flugg\Responder\Transformers\Transformer|string|callable|null $transformer 64 | * @param string|null $resourceKey 65 | * @return \League\Fractal\Resource\ResourceInterface 66 | */ 67 | public function make($data = null, $transformer = null, ?string $resourceKey = null): ResourceInterface 68 | { 69 | if ($data instanceof ResourceInterface) { 70 | return $this->makeFromResource($data, $transformer, $resourceKey); 71 | } elseif (is_null($data = $this->normalizer->normalize($data))) { 72 | return $this->instatiateResource($data, null, $resourceKey); 73 | } 74 | 75 | $transformer = $this->resolveTransformer($data, $transformer); 76 | $resourceKey = $this->resolveResourceKey($data, $resourceKey); 77 | 78 | return $this->instatiateResource($data, $transformer, $resourceKey); 79 | } 80 | 81 | /** 82 | * Make resource from the given resource. 83 | * 84 | * @param \League\Fractal\Resource\ResourceInterface $resource 85 | * @param \Flugg\Responder\Transformers\Transformer|string|callable|null $transformer 86 | * @param string|null $resourceKey 87 | * @return \League\Fractal\Resource\ResourceInterface 88 | */ 89 | public function makeFromResource(ResourceInterface $resource, $transformer = null, ?string $resourceKey = null): ResourceInterface 90 | { 91 | $transformer = $this->resolveTransformer($resource->getData(), $transformer ?: $resource->getTransformer()); 92 | $resourceKey = $this->resolveResourceKey($resource->getData(), $resourceKey ?: $resource->getResourceKey()); 93 | 94 | return $resource->setTransformer($transformer)->setResourceKey($resourceKey); 95 | } 96 | 97 | /** 98 | * Instatiate a new resource instance. 99 | * 100 | * @param mixed $data 101 | * @param \Flugg\Responder\Transformers\Transformer|callable|null $transformer 102 | * @param string|null $resourceKey 103 | * @return \League\Fractal\Resource\ResourceInterface 104 | */ 105 | protected function instatiateResource($data, $transformer = null, ?string $resourceKey = null): ResourceInterface 106 | { 107 | if (is_null($data)) { 108 | return new NullResource(null, null, $resourceKey); 109 | } elseif ($this->shouldCreateCollection($data)) { 110 | return new CollectionResource($data, $transformer, $resourceKey); 111 | } elseif (is_scalar($data)) { 112 | return new Primitive($data, $transformer, $resourceKey); 113 | } 114 | 115 | return new ItemResource($data, $transformer, $resourceKey); 116 | } 117 | 118 | /** 119 | * Indicates if the data belongs to a collection resource. 120 | * 121 | * @param mixed $data 122 | * @return bool 123 | */ 124 | protected function shouldCreateCollection($data): bool 125 | { 126 | if (is_array($data)) { 127 | return ! Arr::isAssoc($data) && ! is_scalar(Arr::first($data)); 128 | } 129 | 130 | return $data instanceof Traversable; 131 | } 132 | 133 | /** 134 | * Resolve a transformer. 135 | * 136 | * @param mixed $data 137 | * @param \Flugg\Responder\Transformers\Transformer|string|callable|null $transformer 138 | * @return \Flugg\Responder\Transformers\Transformer|callable 139 | */ 140 | protected function resolveTransformer($data, $transformer) 141 | { 142 | if (isset($transformer)) { 143 | return $this->transformerResolver->resolve($transformer); 144 | } 145 | 146 | return $this->transformerResolver->resolveFromData($data); 147 | } 148 | 149 | /** 150 | * Resolve a resource key. 151 | * 152 | * @param mixed $data 153 | * @param string|null $resourceKey 154 | * @return null|string 155 | */ 156 | protected function resolveResourceKey($data, ?string $resourceKey = null) 157 | { 158 | return ! empty($resourceKey) ? $resourceKey : $this->resourceKeyResolver->resolve($data); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Resources/ResourceKeyResolver.php: -------------------------------------------------------------------------------- 1 | 14 | * @license The MIT License 15 | */ 16 | class ResourceKeyResolver implements ResourceKeyResolverContract 17 | { 18 | /** 19 | * Transformable to resource key mappings. 20 | * 21 | * @var array 22 | */ 23 | protected $bindings = []; 24 | 25 | /** 26 | * Register a transformable to resource key binding. 27 | * 28 | * @param string|array $transformable 29 | * @param string $resourceKey 30 | * @return void 31 | */ 32 | public function bind($transformable, string $resourceKey) 33 | { 34 | $this->bindings = array_merge($this->bindings, is_array($transformable) ? $transformable : [ 35 | $transformable => $resourceKey, 36 | ]); 37 | } 38 | 39 | /** 40 | * Resolve a resource key from the given data. 41 | * 42 | * @param mixed $data 43 | * @return string 44 | */ 45 | public function resolve($data) 46 | { 47 | $transformable = $this->resolveTransformableItem($data); 48 | 49 | if (is_object($transformable) && key_exists(get_class($transformable), $this->bindings)) { 50 | return $this->bindings[get_class($transformable)]; 51 | } 52 | 53 | if ($transformable instanceof Model) { 54 | return $this->resolveFromModel($transformable); 55 | } 56 | 57 | return 'data'; 58 | } 59 | 60 | /** 61 | * Resolve a resource key from the given model. 62 | * 63 | * @param \Illuminate\Database\Eloquent\Model $model 64 | * @return string 65 | */ 66 | public function resolveFromModel(Model $model) 67 | { 68 | if (method_exists($model, 'getResourceKey')) { 69 | return $model->getResourceKey(); 70 | } 71 | 72 | return $model->getTable(); 73 | } 74 | 75 | /** 76 | * Resolve a transformable item from the given data. 77 | * 78 | * @param mixed $data 79 | * @return mixed 80 | */ 81 | protected function resolveTransformableItem($data) 82 | { 83 | if (is_array($data) || $data instanceof Traversable) { 84 | foreach ($data as $item) { 85 | return $item; 86 | } 87 | } 88 | 89 | return $data; 90 | } 91 | } -------------------------------------------------------------------------------- /src/Responder.php: -------------------------------------------------------------------------------- 1 | 13 | * @license The MIT License 14 | */ 15 | class Responder implements ResponderContract 16 | { 17 | /** 18 | * A builder for building success responses. 19 | * 20 | * @var \Flugg\Responder\Http\Responses\SuccessResponseBuilder 21 | */ 22 | protected $successResponseBuilder; 23 | 24 | /** 25 | * A builder for building error responses. 26 | * 27 | * @var \Flugg\Responder\Http\Responses\ErrorResponseBuilder 28 | */ 29 | protected $errorResponseBuilder; 30 | 31 | /** 32 | * Construct the service class. 33 | * 34 | * @param \Flugg\Responder\Http\Responses\SuccessResponseBuilder $successResponseBuilder 35 | * @param \Flugg\Responder\Http\Responses\ErrorResponseBuilder $errorResponseBuilder 36 | */ 37 | public function __construct(SuccessResponseBuilder $successResponseBuilder, ErrorResponseBuilder $errorResponseBuilder) 38 | { 39 | $this->successResponseBuilder = $successResponseBuilder; 40 | $this->errorResponseBuilder = $errorResponseBuilder; 41 | } 42 | 43 | /** 44 | * Build a successful response. 45 | * 46 | * @param mixed $data 47 | * @param callable|string|\Flugg\Responder\Transformers\Transformer|null $transformer 48 | * @param string|null $resourceKey 49 | * @return \Flugg\Responder\Http\Responses\SuccessResponseBuilder 50 | */ 51 | public function success($data = null, $transformer = null, ?string $resourceKey = null): SuccessResponseBuilder 52 | { 53 | return $this->successResponseBuilder->transform($data, $transformer, $resourceKey); 54 | } 55 | 56 | /** 57 | * Build an error response. 58 | * 59 | * @param mixed|null $errorCode 60 | * @param string|null $message 61 | * @return \Flugg\Responder\Http\Responses\ErrorResponseBuilder 62 | */ 63 | public function error($errorCode = null, ?string $message = null): ErrorResponseBuilder 64 | { 65 | return $this->errorResponseBuilder->error($errorCode, $message); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/ResponderServiceProvider.php: -------------------------------------------------------------------------------- 1 | 40 | * @license The MIT License 41 | */ 42 | class ResponderServiceProvider extends BaseServiceProvider 43 | { 44 | /** 45 | * Indicates if loading of the provider is deferred. 46 | * 47 | * @var bool 48 | */ 49 | protected $defer = false; 50 | 51 | /** 52 | * Register the service provider. 53 | * 54 | * @return void 55 | */ 56 | public function register() 57 | { 58 | if ($this->app instanceof Laravel) { 59 | $this->registerLaravelBindings(); 60 | } elseif ($this->app instanceof Lumen) { 61 | $this->registerLumenBindings(); 62 | } 63 | 64 | $this->registerSerializerBindings(); 65 | $this->registerErrorBindings(); 66 | $this->registerFractalBindings(); 67 | $this->registerTransformerBindings(); 68 | $this->registerResourceBindings(); 69 | $this->registerPaginationBindings(); 70 | $this->registerTransformationBindings(); 71 | $this->registerServiceBindings(); 72 | } 73 | 74 | /** 75 | * Register Laravel bindings. 76 | * 77 | * @return void 78 | */ 79 | protected function registerLaravelBindings() 80 | { 81 | $this->app->singleton(ResponseFactoryContract::class, function ($app) { 82 | return $this->decorateResponseFactory($app->make(LaravelResponseFactory::class)); 83 | }); 84 | } 85 | 86 | /** 87 | * Register Lumen bindings. 88 | * 89 | * @return void 90 | */ 91 | protected function registerLumenBindings() 92 | { 93 | $this->app->singleton(ResponseFactoryContract::class, function ($app) { 94 | return $this->decorateResponseFactory($app->make(LumenResponseFactory::class)); 95 | }); 96 | 97 | $this->app->bind(Translator::class, function ($app) { 98 | return $app['translator']; 99 | }); 100 | } 101 | 102 | /** 103 | * Decorate response factories. 104 | * 105 | * @param \Flugg\Responder\Contracts\ResponseFactory $factory 106 | * @return \Flugg\Responder\Contracts\ResponseFactory 107 | */ 108 | protected function decorateResponseFactory(ResponseFactoryContract $factory): ResponseFactory 109 | { 110 | foreach ($this->app->config['responder.decorators'] as $decorator) { 111 | $factory = new $decorator($factory); 112 | }; 113 | 114 | return $factory; 115 | } 116 | 117 | /** 118 | * Register serializer bindings. 119 | * 120 | * @return void 121 | */ 122 | protected function registerSerializerBindings() 123 | { 124 | $this->app->bind(ErrorSerializerContract::class, function ($app) { 125 | return $app->make($app->config['responder.serializers.error']); 126 | }); 127 | 128 | $this->app->bind(SerializerAbstract::class, function ($app) { 129 | return $app->make($app->config['responder.serializers.success']); 130 | }); 131 | } 132 | 133 | /** 134 | * Register error bindings. 135 | * 136 | * @return void 137 | */ 138 | protected function registerErrorBindings() 139 | { 140 | $this->app->singleton(ErrorMessageResolverContract::class, function ($app) { 141 | return $app->make(ErrorMessageResolver::class); 142 | }); 143 | 144 | $this->app->singleton(ErrorFactoryContract::class, function ($app) { 145 | return $app->make(ErrorFactory::class); 146 | }); 147 | 148 | $this->app->bind(ErrorResponseBuilder::class, function ($app) { 149 | return (new ErrorResponseBuilder($app->make(ResponseFactoryContract::class), $app->make(ErrorFactoryContract::class)))->serializer($app->make(ErrorSerializerContract::class)); 150 | }); 151 | } 152 | 153 | /** 154 | * Register Fractal bindings. 155 | * 156 | * @return void 157 | */ 158 | protected function registerFractalBindings() 159 | { 160 | $this->app->bind(Manager::class, function ($app) { 161 | return (new Manager)->setRecursionLimit($app->config['responder.recursion_limit']); 162 | }); 163 | } 164 | 165 | /** 166 | * Register transformer bindings. 167 | * 168 | * @return void 169 | */ 170 | protected function registerTransformerBindings() 171 | { 172 | $this->app->singleton(TransformerResolverContract::class, function ($app) { 173 | return new TransformerResolver($app, $app->config['responder.fallback_transformer']); 174 | }); 175 | 176 | BaseTransformer::containerResolver(function () { 177 | return $this->app->make(Container::class); 178 | }); 179 | } 180 | 181 | /** 182 | * Register pagination bindings. 183 | * 184 | * @return void 185 | */ 186 | protected function registerResourceBindings() 187 | { 188 | $this->app->singleton(ResourceKeyResolverContract::class, function ($app) { 189 | return $app->make(ResourceKeyResolver::class); 190 | }); 191 | 192 | $this->app->singleton(ResourceFactoryContract::class, function ($app) { 193 | return $app->make(ResourceFactory::class); 194 | }); 195 | } 196 | 197 | /** 198 | * Register pagination bindings. 199 | * 200 | * @return void 201 | */ 202 | protected function registerPaginationBindings() 203 | { 204 | $this->app->bind(PaginatorFactoryContract::class, function ($app) { 205 | return new PaginatorFactory($app->make(Request::class)->query()); 206 | }); 207 | } 208 | 209 | /** 210 | * Register transformation bindings. 211 | * 212 | * @return void 213 | */ 214 | protected function registerTransformationBindings() 215 | { 216 | $this->app->bind(TransformFactoryContract::class, function ($app) { 217 | return $app->make(FractalTransformFactory::class); 218 | }); 219 | 220 | $this->app->bind(TransformBuilder::class, function ($app) { 221 | $request = $this->app->make(Request::class); 222 | $relations = $request->input($this->app->config['responder.load_relations_parameter'], []); 223 | $fieldsets = $request->input($app->config['responder.filter_fields_parameter'], []); 224 | 225 | return (new TransformBuilder($app->make(ResourceFactoryContract::class), $app->make(TransformFactoryContract::class), $app->make(PaginatorFactoryContract::class)))->serializer($app->make(SerializerAbstract::class)) 226 | ->with(is_string($relations) ? explode(',', $relations) : $relations) 227 | ->only($fieldsets); 228 | }); 229 | } 230 | 231 | /** 232 | * Register service bindings. 233 | * 234 | * @return void 235 | */ 236 | protected function registerServiceBindings() 237 | { 238 | $this->app->bind(ResponderContract::class, function ($app) { 239 | return $app->make(Responder::class); 240 | }); 241 | } 242 | 243 | /** 244 | * Bootstrap the application events. 245 | * 246 | * @return void 247 | */ 248 | public function boot() 249 | { 250 | if ($this->app instanceof Laravel) { 251 | $this->bootLaravelApplication(); 252 | } elseif ($this->app instanceof Lumen) { 253 | $this->bootLumenApplication(); 254 | } 255 | 256 | $this->mergeConfigFrom(__DIR__ . '/../config/responder.php', 'responder'); 257 | $this->commands(MakeTransformer::class); 258 | } 259 | 260 | /** 261 | * Bootstrap the Laravel application. 262 | * 263 | * @return void 264 | */ 265 | protected function bootLaravelApplication() 266 | { 267 | if ($this->app->runningInConsole()) { 268 | $this->publishes([ 269 | __DIR__ . '/../config/responder.php' => config_path('responder.php'), 270 | ], 'config'); 271 | $this->publishes([ 272 | __DIR__ . '/../resources/lang/en/errors.php' => base_path('resources/lang/en/errors.php'), 273 | ], 'lang'); 274 | } 275 | } 276 | 277 | /** 278 | * Bootstrap the Lumen application. 279 | * 280 | * @return void 281 | */ 282 | protected function bootLumenApplication() 283 | { 284 | $this->app->configure('responder'); 285 | } 286 | } -------------------------------------------------------------------------------- /src/Serializers/ErrorSerializer.php: -------------------------------------------------------------------------------- 1 | 11 | * @license The MIT License 12 | */ 13 | class ErrorSerializer implements ErrorSerializerContract 14 | { 15 | /** 16 | * Format the error data. 17 | * 18 | * @param mixed|null $errorCode 19 | * @param string|null $message 20 | * @param array|null $data 21 | * @return array 22 | */ 23 | public function format($errorCode = null, ?string $message = null, ?array $data = null): array 24 | { 25 | $response = [ 26 | 'error' => [ 27 | 'code' => $errorCode, 28 | 'message' => $message, 29 | ], 30 | ]; 31 | 32 | if (is_array($data)) { 33 | $response['error'] = array_merge($response['error'], $data); 34 | } 35 | 36 | return $response; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Serializers/NoopSerializer.php: -------------------------------------------------------------------------------- 1 | 16 | * @license The MIT License 17 | */ 18 | class NoopSerializer extends SuccessSerializer 19 | { 20 | /** 21 | * Serialize collection resources. 22 | * 23 | * @param string $resourceKey 24 | * @param array $data 25 | * @return array 26 | */ 27 | public function collection($resourceKey, array $data): array 28 | { 29 | return $data; 30 | } 31 | 32 | /** 33 | * Serialize item resources. 34 | * 35 | * @param string $resourceKey 36 | * @param array $data 37 | * @return array 38 | */ 39 | public function item($resourceKey, array $data): array 40 | { 41 | return $data; 42 | } 43 | 44 | /** 45 | * Serialize null resources. 46 | * 47 | * @return null|array 48 | */ 49 | public function null(): ?array 50 | { 51 | return null; 52 | } 53 | 54 | /** 55 | * Format meta data. 56 | * 57 | * @param array $meta 58 | * @return array 59 | */ 60 | public function meta(array $meta): array 61 | { 62 | return []; 63 | } 64 | 65 | /** 66 | * Format pagination data. 67 | * 68 | * @param \League\Fractal\Pagination\PaginatorInterface $paginator 69 | * @return array 70 | */ 71 | public function paginator(PaginatorInterface $paginator): array 72 | { 73 | return []; 74 | } 75 | 76 | /** 77 | * Format cursor data. 78 | * 79 | * @param \League\Fractal\Pagination\CursorInterface $cursor 80 | * @return array 81 | */ 82 | public function cursor(CursorInterface $cursor): array 83 | { 84 | return []; 85 | } 86 | 87 | /** 88 | * Merge includes into data. 89 | * 90 | * @param array $transformedData 91 | * @param array $includedData 92 | * @return array 93 | */ 94 | public function mergeIncludes($transformedData, $includedData): array 95 | { 96 | return array_merge($transformedData, $includedData); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Serializers/SuccessSerializer.php: -------------------------------------------------------------------------------- 1 | 14 | * @license The MIT License 15 | */ 16 | class SuccessSerializer extends ArraySerializer 17 | { 18 | /** 19 | * Serialize collection resources. 20 | * 21 | * @param string $resourceKey 22 | * @param array $data 23 | * @return array 24 | */ 25 | public function collection($resourceKey, array $data): array 26 | { 27 | return ['data' => $data]; 28 | } 29 | 30 | /** 31 | * Serialize item resources. 32 | * 33 | * @param string $resourceKey 34 | * @param array $data 35 | * @return array 36 | */ 37 | public function item($resourceKey, array $data): array 38 | { 39 | return ['data' => $data]; 40 | } 41 | 42 | /** 43 | * Serialize null resources. 44 | * 45 | * @return null|array 46 | */ 47 | public function null(): ?array 48 | { 49 | return ['data' => null]; 50 | } 51 | 52 | /** 53 | * Format meta data. 54 | * 55 | * @param array $meta 56 | * @return array 57 | */ 58 | public function meta(array $meta): array 59 | { 60 | return $meta; 61 | } 62 | 63 | /** 64 | * Format pagination data. 65 | * 66 | * @param \League\Fractal\Pagination\PaginatorInterface $paginator 67 | * @return array 68 | */ 69 | public function paginator(PaginatorInterface $paginator): array 70 | { 71 | $pagination = parent::paginator($paginator)['pagination']; 72 | 73 | return [ 74 | 'pagination' => [ 75 | 'count' => $pagination['count'], 76 | 'total' => $pagination['total'], 77 | 'perPage' => $pagination['per_page'], 78 | 'currentPage' => $pagination['current_page'], 79 | 'totalPages' => $pagination['total_pages'], 80 | 'links' => $pagination['links'], 81 | ], 82 | ]; 83 | } 84 | 85 | /** 86 | * Format cursor data. 87 | * 88 | * @param \League\Fractal\Pagination\CursorInterface $cursor 89 | * @return array 90 | */ 91 | public function cursor(CursorInterface $cursor): array 92 | { 93 | return [ 94 | 'cursor' => [ 95 | 'current' => $cursor->getCurrent(), 96 | 'previous' => $cursor->getPrev(), 97 | 'next' => $cursor->getNext(), 98 | 'count' => (int) $cursor->getCount(), 99 | ], 100 | ]; 101 | } 102 | 103 | /** 104 | * Merge includes into data. 105 | * 106 | * @param array $transformedData 107 | * @param array $includedData 108 | * @return array 109 | */ 110 | public function mergeIncludes($transformedData, $includedData): array 111 | { 112 | foreach (array_keys($includedData) as $key) { 113 | $includedData[$key] = $includedData[$key]['data']; 114 | } 115 | 116 | return array_merge($transformedData, $includedData); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Testing/MakesApiRequests.php: -------------------------------------------------------------------------------- 1 | 12 | * @license The MIT License 13 | */ 14 | trait MakesApiRequests 15 | { 16 | /** 17 | * Assert that the response is a valid success response. 18 | * 19 | * @param mixed $data 20 | * @param int $status 21 | * @return $this 22 | */ 23 | protected function seeSuccess($data = null, $status = 200) 24 | { 25 | $response = $this->seeSuccessResponse($data, $status); 26 | $this->seeSuccessData($response->getData(true)['data']); 27 | 28 | return $this; 29 | } 30 | 31 | /** 32 | * Assert that the response is a valid success response. 33 | * 34 | * @param mixed $data 35 | * @param int $status 36 | * @return $this 37 | */ 38 | protected function seeSuccessEquals($data = null, $status = 200) 39 | { 40 | $response = $this->seeSuccessResponse($data, $status); 41 | $this->seeJsonEquals($response->getData(true)); 42 | 43 | return $this; 44 | } 45 | 46 | /** 47 | * Assert that the response data contains the given structure. 48 | * 49 | * @param mixed $data 50 | * @return $this 51 | */ 52 | protected function seeSuccessStructure($data = null) 53 | { 54 | $this->seeJsonStructure([ 55 | 'data' => $data, 56 | ]); 57 | 58 | return $this; 59 | } 60 | 61 | /** 62 | * Assert that the response is a valid success response. 63 | * 64 | * @param mixed $data 65 | * @param int $status 66 | * @return \Illuminate\Http\JsonResponse 67 | */ 68 | protected function seeSuccessResponse($data = null, $status = 200): JsonResponse 69 | { 70 | $response = $this->app->make(Responder::class)->success($data, $status); 71 | 72 | $this->seeStatusCode($response->getStatusCode())->seeJson([ 73 | 'success' => true, 74 | 'status' => $response->getStatusCode(), 75 | ])->seeJsonStructure(['data']); 76 | 77 | return $response; 78 | } 79 | 80 | /** 81 | * Assert that the response data contains given values. 82 | * 83 | * @param mixed $data 84 | * @return $this 85 | */ 86 | protected function seeSuccessData($data = null) 87 | { 88 | collect($data)->each(function ($value, $key) { 89 | if (is_array($value)) { 90 | $this->seeSuccessData($value); 91 | } else { 92 | $this->seeJson([$key => $value]); 93 | } 94 | }); 95 | 96 | return $this; 97 | } 98 | 99 | /** 100 | * Decodes JSON response and returns the data. 101 | * 102 | * @param string|array|null $attributes 103 | * @return array 104 | */ 105 | protected function getSuccessData($attributes = null) 106 | { 107 | $rawData = $this->decodeResponseJson()['data']; 108 | 109 | if (is_null($attributes)) { 110 | return $rawData; 111 | } elseif (is_string($attributes)) { 112 | return array_get($rawData, $attributes); 113 | } 114 | 115 | $data = []; 116 | 117 | foreach ($attributes as $attribute) { 118 | $data[] = array_get($rawData, $attribute); 119 | } 120 | 121 | return $data; 122 | } 123 | 124 | /** 125 | * Assert that the response is a valid error response. 126 | * 127 | * @param string $error 128 | * @param int|null $status 129 | * @return $this 130 | */ 131 | protected function seeError(string $error, ?int $status = null) 132 | { 133 | if (! is_null($status)) { 134 | $this->seeStatusCode($status); 135 | } 136 | 137 | if ($this->app->config->get('responder.status_code')) { 138 | $this->seeJson([ 139 | 'status' => $status, 140 | ]); 141 | } 142 | 143 | return $this->seeJson([ 144 | 'success' => false, 145 | ])->seeJsonSubset([ 146 | 'error' => [ 147 | 'code' => $error, 148 | ], 149 | ]); 150 | } 151 | 152 | /** 153 | * Asserts that the status code of the response matches the given code. 154 | * 155 | * @param int $status 156 | * @return $this 157 | */ 158 | abstract protected function seeStatusCode($status); 159 | 160 | /** 161 | * Assert that the response contains JSON. 162 | * 163 | * @param array|null $data 164 | * @param bool $negate 165 | * @return $this 166 | */ 167 | abstract public function seeJson(?array $data = null, $negate = false); 168 | 169 | /** 170 | * Assert that the JSON response has a given structure. 171 | * 172 | * @param array|null $structure 173 | * @param array|null $responseData 174 | * @return $this 175 | */ 176 | abstract public function seeJsonStructure(?array $structure = null, $responseData = null); 177 | 178 | /** 179 | * Assert that the response is a superset of the given JSON. 180 | * 181 | * @param array $data 182 | * @return $this 183 | */ 184 | abstract protected function seeJsonSubset(array $data); 185 | 186 | /** 187 | * Assert that the response contains an exact JSON array. 188 | * 189 | * @param array $data 190 | * @return $this 191 | */ 192 | abstract public function seeJsonEquals(array $data); 193 | 194 | /** 195 | * Validate and return the decoded response JSON. 196 | * 197 | * @return array 198 | */ 199 | abstract protected function decodeResponseJson(); 200 | } 201 | -------------------------------------------------------------------------------- /src/TransformBuilder.php: -------------------------------------------------------------------------------- 1 | 25 | * @license The MIT License 26 | */ 27 | class TransformBuilder 28 | { 29 | /** 30 | * A factory class for making Fractal resources. 31 | * 32 | * @var \Flugg\Responder\Contracts\Resources\ResourceFactory 33 | */ 34 | protected $resourceFactory; 35 | 36 | /** 37 | * A factory for making transformed arrays. 38 | * 39 | * @var \Flugg\Responder\Contracts\TransformFactory 40 | */ 41 | private $transformFactory; 42 | 43 | /** 44 | * A factory used to build Fractal paginator adapters. 45 | * 46 | * @var \Flugg\Responder\Contracts\Pagination\PaginatorFactory 47 | */ 48 | protected $paginatorFactory; 49 | 50 | /** 51 | * The resource that's being built. 52 | * 53 | * @var \League\Fractal\Resource\ResourceInterface 54 | */ 55 | protected $resource; 56 | 57 | /** 58 | * A serializer for formatting data after transforming. 59 | * 60 | * @var \League\Fractal\Serializer\SerializerAbstract 61 | */ 62 | protected $serializer; 63 | 64 | /** 65 | * A list of included relations. 66 | * 67 | * @var array 68 | */ 69 | protected $with = []; 70 | 71 | /** 72 | * A list of excluded relations. 73 | * 74 | * @var array 75 | */ 76 | protected $without = []; 77 | 78 | /** 79 | * A list of sparse fieldsets. 80 | * 81 | * @var array 82 | */ 83 | protected $only = []; 84 | 85 | /** 86 | * Construct the builder class. 87 | * 88 | * @param \Flugg\Responder\Contracts\Resources\ResourceFactory $resourceFactory 89 | * @param \Flugg\Responder\Contracts\TransformFactory $transformFactory 90 | * @param \Flugg\Responder\Contracts\Pagination\PaginatorFactory $paginatorFactory 91 | */ 92 | public function __construct(ResourceFactory $resourceFactory, TransformFactory $transformFactory, PaginatorFactory $paginatorFactory) 93 | { 94 | $this->resourceFactory = $resourceFactory; 95 | $this->transformFactory = $transformFactory; 96 | $this->paginatorFactory = $paginatorFactory; 97 | } 98 | 99 | /** 100 | * Make a resource from the given data and transformer and set the resource key. 101 | * 102 | * @param mixed $data 103 | * @param \Flugg\Responder\Transformers\Transformer|callable|string|null $transformer 104 | * @param string|null $resourceKey 105 | * @return $this 106 | */ 107 | public function resource($data = null, $transformer = null, ?string $resourceKey = null) 108 | { 109 | $this->resource = $this->resourceFactory->make($data, $transformer, $resourceKey); 110 | 111 | if ($data instanceof CursorPaginator) { 112 | $this->cursor($this->paginatorFactory->makeCursor($data)); 113 | } elseif ($data instanceof LengthAwarePaginator) { 114 | $this->paginator($this->paginatorFactory->make($data)); 115 | } 116 | 117 | return $this; 118 | } 119 | 120 | /** 121 | * Manually set the cursor on the resource. 122 | * 123 | * @param \League\Fractal\Pagination\Cursor $cursor 124 | * @return $this 125 | */ 126 | public function cursor(Cursor $cursor) 127 | { 128 | if ($this->resource instanceof CollectionResource) { 129 | $this->resource->setCursor($cursor); 130 | } 131 | 132 | return $this; 133 | } 134 | 135 | /** 136 | * Manually set the paginator on the resource. 137 | * 138 | * @param \League\Fractal\Pagination\IlluminatePaginatorAdapter $paginator 139 | * @return $this 140 | */ 141 | public function paginator(IlluminatePaginatorAdapter $paginator) 142 | { 143 | if ($this->resource instanceof CollectionResource) { 144 | $this->resource->setPaginator($paginator); 145 | } 146 | 147 | return $this; 148 | } 149 | 150 | /** 151 | * Add meta data appended to the response data. 152 | * 153 | * @param array $data 154 | * @return $this 155 | */ 156 | public function meta(array $data) 157 | { 158 | $this->resource->setMeta($data); 159 | 160 | return $this; 161 | } 162 | 163 | /** 164 | * Include relations to the transform. 165 | * 166 | * @param string[]|string $relations 167 | * @return $this 168 | */ 169 | public function with($relations) 170 | { 171 | $relations = is_array($relations) ? $relations : func_get_args(); 172 | 173 | foreach ($relations as $relation => $constraint) { 174 | if (is_numeric($relation)) { 175 | $relation = $constraint; 176 | $constraint = null; 177 | } 178 | 179 | $this->with = array_merge($this->with, [$relation => $constraint]); 180 | } 181 | 182 | return $this; 183 | } 184 | 185 | /** 186 | * Exclude relations from the transform. 187 | * 188 | * @param string[]|string $relations 189 | * @return $this 190 | */ 191 | public function without($relations) 192 | { 193 | $this->without = array_merge($this->without, is_array($relations) ? $relations : func_get_args()); 194 | 195 | return $this; 196 | } 197 | 198 | /** 199 | * Filter fields to output using sparse fieldsets. 200 | * 201 | * @param string[]|string $fields 202 | * @return $this 203 | */ 204 | public function only($fields) 205 | { 206 | $this->only = array_merge($this->only, is_array($fields) ? $fields : func_get_args()); 207 | 208 | return $this; 209 | } 210 | 211 | /** 212 | * Set the serializer. 213 | * 214 | * @param \League\Fractal\Serializer\SerializerAbstract|string $serializer 215 | * @return $this 216 | * 217 | * @throws \Flugg\Responder\Exceptions\InvalidSuccessSerializerException 218 | */ 219 | public function serializer($serializer) 220 | { 221 | if (is_string($serializer)) { 222 | $serializer = new $serializer; 223 | } 224 | 225 | if (! $serializer instanceof SerializerAbstract) { 226 | throw new InvalidSuccessSerializerException; 227 | } 228 | 229 | $this->serializer = $serializer; 230 | 231 | return $this; 232 | } 233 | 234 | /** 235 | * Transform and serialize the data and return the transformed array. 236 | * 237 | * @return array|null 238 | */ 239 | public function transform() 240 | { 241 | $this->prepareRelations($this->resource->getData(), $this->resource->getTransformer()); 242 | 243 | return $this->transformFactory->make($this->resource ?: new NullResource, $this->serializer, [ 244 | 'includes' => $this->with, 245 | 'excludes' => $this->without, 246 | 'fieldsets' => $this->only, 247 | ]); 248 | } 249 | 250 | /** 251 | * Prepare requested relations for the transformation. 252 | * 253 | * @param mixed $data 254 | * @param \Flugg\Responder\Transformers\Transformer|callable|string|null $transformer 255 | * @return void 256 | */ 257 | protected function prepareRelations($data, $transformer) 258 | { 259 | if ($transformer instanceof Transformer) { 260 | $relations = $transformer->relations($this->with); 261 | $defaultRelations = $this->removeExcludedRelations($transformer->defaultRelations($this->with)); 262 | $this->with = array_merge($relations, $defaultRelations); 263 | } 264 | 265 | if ($data instanceof Model || $data instanceof Collection) { 266 | $this->eagerLoadRelations($data, $this->with, $transformer); 267 | } 268 | 269 | $this->with = array_keys($this->with); 270 | } 271 | 272 | /** 273 | * Filter out relations that have been explicitly excluded using the [without] method. 274 | * 275 | * @param array $relations 276 | * @return array 277 | */ 278 | protected function removeExcludedRelations(array $relations): array 279 | { 280 | return array_filter($relations, function ($relation) { 281 | return ! in_array($this->stripParametersFromRelation($relation), $this->without); 282 | }, ARRAY_FILTER_USE_KEY); 283 | } 284 | 285 | /** 286 | * Strip parameter suffix from the relation string by only taking what is in front of 287 | * the colon. 288 | * 289 | * @param string $relation 290 | * @return string 291 | */ 292 | protected function stripParametersFromRelation(string $relation): string 293 | { 294 | return explode(':', $relation)[0]; 295 | } 296 | 297 | /** 298 | * Eager load all requested relations except the ones defined as an "include" method 299 | * in the transformers. We also strip away any parameters from the relation name 300 | * and normalize relations by swapping "null" constraints to empty closures. 301 | * 302 | * @param mixed $data 303 | * @param array $requested 304 | * @param \Flugg\Responder\Transformers\Transformer|callable|string|null $transformer 305 | * @return void 306 | */ 307 | protected function eagerLoadRelations($data, array $requested, $transformer) 308 | { 309 | $relations = collect(array_keys($requested))->reduce(function ($eagerLoads, $relation) use ($requested, $transformer) { 310 | $identifier = $this->stripParametersFromRelation($relation); 311 | 312 | if (config('responder.use_camel_case_relations')) { 313 | $identifier = Str::camel($identifier); 314 | } 315 | 316 | if (method_exists($transformer, 'include'.ucfirst($identifier))) { 317 | return $eagerLoads; 318 | } 319 | 320 | return array_merge($eagerLoads, [$identifier => $requested[$relation] ?: function () { 321 | }]); 322 | }, []); 323 | 324 | $data->loadMissing($relations); 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /src/Transformation.php: -------------------------------------------------------------------------------- 1 | 11 | * @license The MIT License 12 | */ 13 | class Transformation 14 | { 15 | /** 16 | * A builder used to build transformed arrays. 17 | * 18 | * @var \Flugg\Responder\TransformBuilder 19 | */ 20 | protected $transformBuilder; 21 | 22 | /** 23 | * Construct the service class. 24 | * 25 | * @param \Flugg\Responder\TransformBuilder $transformBuilder 26 | */ 27 | public function __construct(TransformBuilder $transformBuilder) 28 | { 29 | $this->transformBuilder = $transformBuilder; 30 | } 31 | 32 | /** 33 | * Make a new transformation to transform data without serializing. 34 | * 35 | * @param mixed $data 36 | * @param \Flugg\Responder\Transformers\Transformer|callable|string|null $transformer 37 | * @param string|null $resourceKey 38 | * @return \Flugg\Responder\TransformBuilder 39 | */ 40 | public function make($data = null, $transformer = null, ?string $resourceKey = null): TransformBuilder 41 | { 42 | return $this->transformBuilder->resource($data, $transformer, $resourceKey)->serializer(new NoopSerializer); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Transformers/ArrayTransformer.php: -------------------------------------------------------------------------------- 1 | 12 | * @license The MIT License 13 | */ 14 | class ArrayTransformer extends Transformer 15 | { 16 | /** 17 | * Transform the data. 18 | * 19 | * @param mixed $data 20 | * @return array 21 | */ 22 | public function transform($data) 23 | { 24 | return $data instanceof Arrayable ? $data->toArray() : $data; 25 | } 26 | } -------------------------------------------------------------------------------- /src/Transformers/Concerns/HasRelationships.php: -------------------------------------------------------------------------------- 1 | 14 | * @license The MIT License 15 | */ 16 | trait HasRelationships 17 | { 18 | /** 19 | * List of available relations. 20 | * 21 | * @var string[] 22 | */ 23 | protected $relations = []; 24 | 25 | /** 26 | * A list of autoloaded default relations. 27 | * 28 | * @var array 29 | */ 30 | protected $load = []; 31 | 32 | /** 33 | * Get a list of whitelisted relations that are requested, including nested relations. 34 | * 35 | * @param array $requested 36 | * @return array 37 | */ 38 | public function relations(array $requested = []): array 39 | { 40 | $requested = $this->normalizeRelations($requested); 41 | $relations = $this->applyQueryConstraints($this->extractRelations($requested)); 42 | $nestedRelations = $this->nestedRelations($requested, $relations, 'relations'); 43 | 44 | return array_merge($relations, $nestedRelations); 45 | } 46 | 47 | /** 48 | * Get a list of default relations including nested relations. 49 | * 50 | * @param array $requested 51 | * @return array 52 | */ 53 | public function defaultRelations(array $requested = []): array 54 | { 55 | $requested = $this->normalizeRelations($requested); 56 | $relations = $this->applyQueryConstraints($this->normalizeRelations($this->load)); 57 | $nestedRelations = $this->nestedRelations($relations, array_merge($relations, $requested), 'defaultRelations'); 58 | 59 | return array_merge($relations, $nestedRelations); 60 | } 61 | 62 | /** 63 | * Get a list of available relations from the transformer with a normalized structure. 64 | * 65 | * @return array 66 | */ 67 | protected function availableRelations(): array 68 | { 69 | return $this->normalizeRelations(array_merge($this->relations, $this->load)); 70 | } 71 | 72 | /** 73 | * Get nested relations from transformers resolved from the $available parameter that 74 | * also occur in the $requested parameter. 75 | * 76 | * @param array $requested 77 | * @param array $available 78 | * @param string $method 79 | * @return array 80 | */ 81 | protected function nestedRelations(array $requested, array $available, string $method): array 82 | { 83 | $transformers = $this->mappedTransformers($available); 84 | 85 | return collect(array_keys($transformers))->reduce(function ($nestedRelations, $relation) use ($requested, $method, $transformers) { 86 | $transformer = $transformers[$relation]; 87 | $children = $this->extractChildRelations($requested, $relation); 88 | $childRelations = $this->wrapChildRelations($transformer->$method($children), $relation); 89 | 90 | return array_merge($nestedRelations, $childRelations); 91 | }, []); 92 | } 93 | 94 | /** 95 | * Extract available root relations from the given list of relations. 96 | * 97 | * @param array $relations 98 | * @return array 99 | */ 100 | protected function extractRelations(array $relations): array 101 | { 102 | $available = $this->availableRelations(); 103 | 104 | return array_filter($this->mapRelations($relations, function ($relation, $constraint) { 105 | $identifier = explode('.', $relation)[0]; 106 | $constraint = $identifier === $relation ? $constraint : null; 107 | 108 | return [$identifier => $constraint ?: $this->resolveQueryConstraint($identifier)]; 109 | }), function ($relation) use ($available) { 110 | return Arr::has($available, explode(':', $relation)[0]); 111 | }, ARRAY_FILTER_USE_KEY); 112 | } 113 | 114 | /** 115 | * Extract all nested relations under a given identifier. 116 | * 117 | * @param array $relations 118 | * @param string $identifier 119 | * @return array 120 | */ 121 | protected function extractChildRelations(array $relations, string $identifier): array 122 | { 123 | return array_reduce(array_keys($relations), function ($nested, $relation) use ($relations, $identifier) { 124 | if (! Str::startsWith($relation, "$identifier.")) { 125 | return $nested; 126 | } 127 | 128 | $nestedIdentifier = explode('.', $relation); 129 | array_shift($nestedIdentifier); 130 | 131 | return array_merge($nested, [implode('.', $nestedIdentifier) => $relations[$relation]]); 132 | }, []); 133 | } 134 | 135 | /** 136 | * Wrap the identifier of each relation of the given list of nested relations with 137 | * the parent relation identifier using dot notation. 138 | * 139 | * @param array $nestedRelations 140 | * @param string $relation 141 | * @return array 142 | */ 143 | protected function wrapChildRelations(array $nestedRelations, string $relation): array 144 | { 145 | return $this->mapRelations($nestedRelations, function ($nestedRelation, $constraint) use ($relation) { 146 | return ["$relation.$nestedRelation" => $constraint]; 147 | }); 148 | } 149 | 150 | /** 151 | * Normalize relations to force an [identifier => constraint/transformer] structure. 152 | * 153 | * @param array $relations 154 | * @return array 155 | */ 156 | protected function normalizeRelations(array $relations): array 157 | { 158 | return array_reduce(array_keys($relations), function ($normalized, $relation) use ($relations) { 159 | if (is_numeric($relation)) { 160 | return array_merge($normalized, [$relations[$relation] => null]); 161 | } 162 | 163 | return array_merge($normalized, [$relation => $relations[$relation]]); 164 | }, []); 165 | } 166 | 167 | /** 168 | * Map over a list of relations with the [identifier => constraint/transformer] structure. 169 | * 170 | * @param array $relations 171 | * @param callable $callback 172 | * @return array 173 | */ 174 | protected function mapRelations(array $relations, callable $callback): array 175 | { 176 | $mapped = []; 177 | 178 | foreach ($relations as $identifier => $value) { 179 | $mapped = array_merge($mapped, $callback($identifier, $value)); 180 | } 181 | 182 | return $mapped; 183 | } 184 | 185 | /** 186 | * Applies any query constraints defined in the transformer to the list of relaations. 187 | * 188 | * @param array $relations 189 | * @return array 190 | */ 191 | protected function applyQueryConstraints(array $relations): array 192 | { 193 | return $this->mapRelations($relations, function ($relation, $constraint) { 194 | return [$relation => is_callable($constraint) ? $constraint : $this->resolveQueryConstraint($relation)]; 195 | }); 196 | } 197 | 198 | /** 199 | * Resolve a query constraint for a given relation identifier. 200 | * 201 | * @param string $identifier 202 | * @return \Closure|null 203 | */ 204 | protected function resolveQueryConstraint(string $identifier) 205 | { 206 | if(config('responder.use_camel_case_relations')) { 207 | $identifier = Str::camel($identifier); 208 | } 209 | 210 | if (! method_exists($this, $method = 'load' . ucfirst($identifier))) { 211 | return null; 212 | } 213 | 214 | return function ($query) use ($method) { 215 | return $this->$method($query); 216 | }; 217 | } 218 | 219 | /** 220 | * Resolve a relation from a model instance and an identifier. 221 | * 222 | * @param \Illuminate\Database\Eloquent\Model $model 223 | * @param string $identifier 224 | * @return mixed 225 | */ 226 | protected function resolveRelation(Model $model, string $identifier) 227 | { 228 | if(config('responder.use_camel_case_relations')) { 229 | $identifier = Str::camel($identifier); 230 | } 231 | 232 | $relation = $model->$identifier; 233 | 234 | if (method_exists($this, $method = 'filter' . ucfirst($identifier))) { 235 | return $this->$method($relation); 236 | } 237 | 238 | return $relation; 239 | } 240 | 241 | /** 242 | * Resolve a list of transformers from a list of relations mapped to transformers. 243 | * 244 | * @param array $relations 245 | * @return array 246 | */ 247 | protected function mappedTransformers(array $relations): array 248 | { 249 | $transformers = collect($this->availableRelations())->filter(function ($transformer) { 250 | return ! is_null($transformer); 251 | })->map(function ($transformer) { 252 | return $this->resolveTransformer($transformer); 253 | })->all(); 254 | 255 | return array_intersect_key($transformers, $relations); 256 | } 257 | 258 | /** 259 | * Get a related transformer class mapped to a relation identifier. 260 | * 261 | * @param string $identifier 262 | * @return string|null 263 | */ 264 | protected function mappedTransformerClass(string $identifier) 265 | { 266 | return $this->availableRelations()[$identifier] ?? null; 267 | } 268 | 269 | /** 270 | * Resolve a transformer from a class name string. 271 | * 272 | * @param string $transformer 273 | * @return mixed 274 | */ 275 | protected abstract function resolveTransformer(string $transformer); 276 | } -------------------------------------------------------------------------------- /src/Transformers/Concerns/MakesResources.php: -------------------------------------------------------------------------------- 1 | 17 | * @license The MIT License 18 | */ 19 | trait MakesResources 20 | { 21 | /** 22 | * A list of cached related resources. 23 | * 24 | * @var \League\Fractal\ResourceInterface[] 25 | */ 26 | protected $resources = []; 27 | 28 | /** 29 | * Make a resource. 30 | * 31 | * @param mixed $data 32 | * @param \Flugg\Responder\Transformers\Transformer|string|callable|null $transformer 33 | * @param string|null $resourceKey 34 | * @return \League\Fractal\Resource\ResourceInterface 35 | */ 36 | protected function resource($data = null, $transformer = null, ?string $resourceKey = null): ResourceInterface 37 | { 38 | if ($data instanceof ResourceInterface) { 39 | return $data; 40 | } 41 | 42 | $resourceFactory = $this->resolveContainer()->make(ResourceFactory::class); 43 | 44 | return $resourceFactory->make($data, $transformer, $resourceKey); 45 | } 46 | 47 | /** 48 | * Include a related resource. 49 | * 50 | * @param string $identifier 51 | * @param mixed $data 52 | * @param array $parameters 53 | * @return \League\Fractal\Resource\ResourceInterface 54 | * 55 | * @throws \LogicException 56 | */ 57 | protected function includeResource(string $identifier, $data, array $parameters): ResourceInterface 58 | { 59 | $transformer = $this->mappedTransformerClass($identifier); 60 | 61 | if (config('responder.use_camel_case_relations')) { 62 | $identifier = Str::camel($identifier); 63 | } 64 | 65 | if (method_exists($this, $method = 'include'.ucfirst($identifier))) { 66 | $resource = $this->resource($this->$method($data, collect($parameters)), $transformer, $identifier); 67 | } elseif ($data instanceof Model) { 68 | $resource = $this->includeResourceFromModel($data, $identifier, $transformer); 69 | } else { 70 | throw new LogicException('Relation ['.$identifier.'] not found in ['.get_class($this).'].'); 71 | } 72 | 73 | return $resource; 74 | } 75 | 76 | /** 77 | * Include a related resource from a model and cache the resource type for following calls. 78 | * 79 | * @param \Illuminate\Database\Eloquent\Model $model 80 | * @param string $identifier 81 | * @param \Flugg\Responder\Transformers\Transformer|string|callable|null $transformer 82 | * @return \League\Fractal\Resource\ResourceInterface 83 | */ 84 | protected function includeResourceFromModel(Model $model, string $identifier, $transformer = null): ResourceInterface 85 | { 86 | $data = $this->resolveRelation($model, $identifier); 87 | 88 | if (! $this->shouldCacheResource($data)) { 89 | return $this->resource($data, $transformer, $identifier); 90 | } elseif (key_exists($identifier, $this->resources)) { 91 | return $this->resources[$identifier]->setData($data); 92 | } 93 | 94 | return $this->resources[$identifier] = $this->resource($data, $transformer, $identifier); 95 | } 96 | 97 | /** 98 | * Indicates if the resource should be cached. 99 | * 100 | * @param mixed $data 101 | * @return bool 102 | */ 103 | protected function shouldCacheResource($data): bool 104 | { 105 | return is_array($data) || $data instanceof Countable ? count($data) > 0 : is_null($data); 106 | } 107 | 108 | /** 109 | * Resolve a container using the resolver callback. 110 | * 111 | * @return \Illuminate\Contracts\Container\Container 112 | */ 113 | abstract protected function resolveContainer(): Container; 114 | 115 | /** 116 | * Resolve relation data from a model. 117 | * 118 | * @param \Illuminate\Database\Eloquent\Model $model 119 | * @param string $identifier 120 | * @return mixed 121 | */ 122 | abstract protected function resolveRelation(Model $model, string $identifier); 123 | 124 | /** 125 | * Get a related transformer class mapped to a relation identifier. 126 | * 127 | * @param string $identifier 128 | * @return string 129 | */ 130 | abstract protected function mappedTransformerClass(string $identifier); 131 | } 132 | -------------------------------------------------------------------------------- /src/Transformers/Concerns/OverridesFractal.php: -------------------------------------------------------------------------------- 1 | 13 | * @license The MIT License 14 | */ 15 | trait OverridesFractal 16 | { 17 | /** 18 | * Overrides Fractal's getter for available includes. 19 | * 20 | * @return array 21 | */ 22 | public function getAvailableIncludes(): array 23 | { 24 | if ($this->relations == ['*']) { 25 | return $this->resolveScopedIncludes($this->getCurrentScope()); 26 | } 27 | 28 | return array_keys($this->normalizeRelations($this->relations)); 29 | } 30 | 31 | /** 32 | * Overrides Fractal's getter for default includes. 33 | * 34 | * @return array 35 | */ 36 | public function getDefaultIncludes(): array 37 | { 38 | return array_keys($this->normalizeRelations($this->load)); 39 | } 40 | 41 | /** 42 | * Overrides Fractal's method for including a relation. 43 | * 44 | * @param \League\Fractal\Scope $scope 45 | * @param string $identifier 46 | * @param mixed $data 47 | * @return \League\Fractal\Resource\ResourceInterface 48 | */ 49 | protected function callIncludeMethod(Scope $scope, $identifier, $data) 50 | { 51 | $parameters = iterator_to_array($scope->getManager()->getIncludeParams($scope->getIdentifier($identifier))); 52 | 53 | return $this->includeResource($identifier, $data, $parameters); 54 | } 55 | 56 | /** 57 | * Resolve scoped includes for the given scope. 58 | * 59 | * @param \League\Fractal\Scope $scope 60 | * @return array 61 | */ 62 | protected function resolveScopedIncludes(Scope $scope): array 63 | { 64 | $level = count($scope->getParentScopes()); 65 | $includes = $scope->getManager()->getRequestedIncludes(); 66 | 67 | return collect($includes)->map(function ($include) { 68 | return explode('.', $include); 69 | })->filter(function ($include) use ($level) { 70 | return count($include) > $level; 71 | })->pluck($level)->unique()->all(); 72 | } 73 | 74 | /** 75 | * Get the current scope of the transformer. 76 | * 77 | * @return \League\Fractal\Scope 78 | */ 79 | public abstract function getCurrentScope(); 80 | 81 | /** 82 | * Normalize relations to force a key value structure. 83 | * 84 | * @param array $relations 85 | * @return array 86 | */ 87 | protected abstract function normalizeRelations(array $relations): array; 88 | 89 | /** 90 | * Include a related resource. 91 | * 92 | * @param string $identifier 93 | * @param mixed $data 94 | * @param array $parameters 95 | * @return \League\Fractal\Resource\ResourceInterface 96 | */ 97 | protected abstract function includeResource(string $identifier, $data, array $parameters): ResourceInterface; 98 | } -------------------------------------------------------------------------------- /src/Transformers/Transformer.php: -------------------------------------------------------------------------------- 1 | 18 | * @license The MIT License 19 | */ 20 | abstract class Transformer extends TransformerAbstract 21 | { 22 | use HasRelationships; 23 | use MakesResources; 24 | use OverridesFractal; 25 | 26 | /** 27 | * The container resolver callback. 28 | * 29 | * @var \Closure|null 30 | */ 31 | protected static $containerResolver; 32 | 33 | /** 34 | * Set a container using a resolver callback. 35 | * 36 | * @param \Closure $resolver 37 | * @return void 38 | */ 39 | public static function containerResolver(Closure $resolver) 40 | { 41 | static::$containerResolver = $resolver; 42 | } 43 | 44 | /** 45 | * Resolve a container using the resolver callback. 46 | * 47 | * @return \Illuminate\Contracts\Container\Container 48 | */ 49 | protected function resolveContainer(): Container 50 | { 51 | return call_user_func(static::$containerResolver); 52 | } 53 | 54 | /** 55 | * Resolve a transformer from a class name string. 56 | * 57 | * @param string $transformer 58 | * @return mixed 59 | */ 60 | protected function resolveTransformer(string $transformer) 61 | { 62 | $transformerResolver = $this->resolveContainer()->make(TransformerResolver::class); 63 | 64 | return $transformerResolver->resolve($transformer); 65 | } 66 | } -------------------------------------------------------------------------------- /src/Transformers/TransformerResolver.php: -------------------------------------------------------------------------------- 1 | 16 | * @license The MIT License 17 | */ 18 | class TransformerResolver implements TransformerResolverContract 19 | { 20 | /** 21 | * Transformable to transformer mappings. 22 | * 23 | * @var array 24 | */ 25 | protected $bindings = []; 26 | 27 | /** 28 | * A container used to resolve transformers. 29 | * 30 | * @var \Illuminate\Contracts\Container\Container 31 | */ 32 | protected $container; 33 | 34 | /** 35 | * A fallback transformer to return when no transformer can be resolved. 36 | * 37 | * @var \Flugg\Responder\Transformers\Transformer|string|callable 38 | */ 39 | protected $fallback; 40 | 41 | /** 42 | * Construct the resolver class. 43 | * 44 | * @param \Illuminate\Contracts\Container\Container $container 45 | * @param \Flugg\Responder\Transformers\Transformer|string|callable $fallback 46 | */ 47 | public function __construct(Container $container, $fallback) 48 | { 49 | $this->container = $container; 50 | $this->fallback = $fallback; 51 | } 52 | 53 | /** 54 | * Register a transformable to transformer binding. 55 | * 56 | * @param string|array $transformable 57 | * @param string|callback|null $transformer 58 | * @return void 59 | */ 60 | public function bind($transformable, $transformer = null) 61 | { 62 | $this->bindings = array_merge($this->bindings, is_array($transformable) ? $transformable : [ 63 | $transformable => $transformer, 64 | ]); 65 | } 66 | 67 | /** 68 | * Resolve a transformer. 69 | * 70 | * @param \Flugg\Responder\Transformers\Transformer|string|callable $transformer 71 | * @return \Flugg\Responder\Transformers\Transformer|callable 72 | * @throws \Flugg\Responder\Exceptions\InvalidTransformerException 73 | */ 74 | public function resolve($transformer) 75 | { 76 | if (is_string($transformer)) { 77 | return $this->container->make($transformer); 78 | } 79 | 80 | if (! is_callable($transformer) && ! $transformer instanceof Transformer) { 81 | throw new InvalidTransformerException; 82 | } 83 | 84 | return $transformer; 85 | } 86 | 87 | /** 88 | * Resolve a transformer from the given data. 89 | * 90 | * @param mixed $data 91 | * @return \Flugg\Responder\Transformers\Transformer|callable 92 | */ 93 | public function resolveFromData($data) 94 | { 95 | $transformer = $this->resolveTransformer($this->resolveTransformableItem($data)); 96 | 97 | return $this->resolve($transformer); 98 | } 99 | 100 | /** 101 | * Resolve a transformer from the transformable element. 102 | * 103 | * @param mixed $transformable 104 | * @return \Flugg\Responder\Contracts\Transformable|callable 105 | */ 106 | protected function resolveTransformer($transformable) 107 | { 108 | if (is_object($transformable) && key_exists(get_class($transformable), $this->bindings)) { 109 | return $this->bindings[get_class($transformable)]; 110 | } 111 | 112 | if ($transformable instanceof Transformable) { 113 | return $transformable->transformer(); 114 | } 115 | 116 | return $this->resolve($this->fallback); 117 | } 118 | 119 | /** 120 | * Resolve a transformable item from the given data. 121 | * 122 | * @param mixed $data 123 | * @return mixed 124 | */ 125 | protected function resolveTransformableItem($data) 126 | { 127 | if (is_array($data) || $data instanceof Traversable) { 128 | foreach ($data as $item) { 129 | return $item; 130 | } 131 | } 132 | 133 | return $data; 134 | } 135 | } -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | make($data, $transformer); 32 | } 33 | } 34 | --------------------------------------------------------------------------------