├── .github ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── config.yml ├── SECURITY.md └── workflows │ ├── php-cs-fixer.yml │ └── run-tests.yml ├── .gitignore ├── .php-cs-fixer.dist.php ├── LICENSE.md ├── README.md ├── composer.json ├── phpstan.neon ├── phpunit.xml.dist ├── publishable └── config │ └── api.php ├── src ├── APIResource.php ├── APIResourceManager.php ├── APIResourcesServiceProvider.php ├── Exceptions │ ├── APIDeprecatedException.php │ └── ResourceNotFoundException.php ├── Facades │ └── APIResource.php ├── Middleware │ ├── APIdeprecated.php │ └── APIversion.php └── helpers.php └── tests ├── APIConfigTest.php ├── APIResourceCollectionTest.php ├── APIResourceTest.php ├── APIResourcesMultipleTest.php ├── APIRouteTest.php ├── BasicTest.php ├── Fixtures ├── Arrayable.php ├── Models │ ├── Post.php │ ├── Rank.php │ └── User.php ├── Resources │ ├── Api │ │ ├── v1 │ │ │ ├── Posts │ │ │ │ └── Single.php │ │ │ └── User.php │ │ └── v2 │ │ │ ├── Post.php │ │ │ ├── Rank.php │ │ │ └── User.php │ ├── App │ │ ├── v1 │ │ │ ├── Posts │ │ │ │ └── Single.php │ │ │ └── User.php │ │ └── v2 │ │ │ ├── Post.php │ │ │ ├── Rank.php │ │ │ ├── User.php │ │ │ └── Users.php │ ├── Collections │ │ ├── v1 │ │ │ └── User.php │ │ └── v2 │ │ │ ├── User.php │ │ │ └── UserCollection.php │ └── v1 │ │ └── User.php └── config │ ├── multi.php │ └── simple.php ├── ResourcePathResolveTest.php └── TestCase.php /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 11 | extremely unfair for them to suffer abuse or anger for their hard work. 12 | 13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 14 | world that developers are civilized and selfless people. 15 | 16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 17 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 18 | 19 | ## Viability 20 | 21 | When requesting or submitting new features, first consider whether it might be useful to others. Open 22 | source projects are used by many developers, who may have entirely different needs to your own. Think about 23 | whether or not your feature is likely to be used by other users of the project. 24 | 25 | ## Procedure 26 | 27 | Before filing an issue: 28 | 29 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 30 | - Check to make sure your feature suggestion isn't already present within the project. 31 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 32 | - Check the pull requests tab to ensure that the feature isn't already in progress. 33 | 34 | Before submitting a pull request: 35 | 36 | - Check the codebase to ensure that your feature doesn't already exist. 37 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 38 | 39 | ## Requirements 40 | 41 | If the project maintainer has any additional requirements, you will find them listed here. 42 | 43 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). 44 | 45 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 46 | 47 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 48 | 49 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. 50 | 51 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 52 | 53 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 54 | 55 | **Happy coding**! 56 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Juampi92 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a question 4 | url: https://github.com/juampi92/api-resources/discussions/new?category=q-a 5 | about: Ask the community for help 6 | - name: Request a feature 7 | url: https://github.com/juampi92/api-resources/discussions/new?category=ideas 8 | about: Share ideas for new features 9 | - name: Report a bug 10 | url: https://github.com/juampi92/api-resources/issues/new 11 | about: Report a reproducable bug 12 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | If you discover any security related issues, please email juampi92@gmail.com instead of using the issue tracker. 4 | -------------------------------------------------------------------------------- /.github/workflows/php-cs-fixer.yml: -------------------------------------------------------------------------------- 1 | name: Check & fix styling 2 | 3 | on: [push] 4 | 5 | jobs: 6 | php-cs-fixer: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v2 12 | with: 13 | ref: ${{ github.head_ref }} 14 | 15 | - name: Run PHP CS Fixer 16 | uses: docker://oskarstark/php-cs-fixer-ga 17 | with: 18 | args: --config=.php-cs-fixer.dist.php --allow-risky=yes 19 | 20 | - name: Commit changes 21 | uses: stefanzweifel/git-auto-commit-action@v4 22 | with: 23 | commit_message: Fix styling 24 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | runs-on: ${{ matrix.os }} 14 | 15 | strategy: 16 | fail-fast: true 17 | matrix: 18 | os: [ubuntu-latest] 19 | php: [8.2, 8.1, 8.0, 7.4] 20 | laravel: ['7.*', '8.*', '9.*', '10.*', '11.*'] 21 | stability: [prefer-stable] 22 | include: 23 | - laravel: 10.* 24 | testbench: 8.* 25 | - laravel: 9.* 26 | testbench: 7.* 27 | - laravel: 8.* 28 | testbench: 6.23 29 | - laravel: 7.* 30 | testbench: 5.2 31 | - laravel: 11.* 32 | testbench: 9.* 33 | exclude: 34 | - laravel: 10.* 35 | php: 8.0 36 | - laravel: 10.* 37 | php: 7.4 38 | - laravel: 9.* 39 | php: 8.0 40 | - laravel: 9.* 41 | php: 7.4 42 | - laravel: 8.* 43 | php: 7.4 44 | - laravel: 7.* 45 | php: 8.1 46 | - laravel: 7.* 47 | php: 8.2 48 | - laravel: 11.* 49 | php: 8.1 50 | - laravel: 11.* 51 | php: 8.0 52 | - laravel: 11.* 53 | php: 7.4 54 | 55 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} 56 | 57 | steps: 58 | - name: Checkout code 59 | uses: actions/checkout@v3 60 | 61 | - name: Setup PHP 62 | uses: shivammathur/setup-php@v2 63 | with: 64 | php-version: ${{ matrix.php }} 65 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo 66 | coverage: none 67 | 68 | - name: Setup problem matchers 69 | run: | 70 | echo "::add-matcher::${{ runner.tool_cache }}/php.json" 71 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 72 | 73 | - name: Install dependencies 74 | run: | 75 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 76 | composer update --${{ matrix.stability }} --prefer-dist --no-interaction 77 | 78 | - name: Execute tests 79 | run: vendor/bin/phpunit 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.idea 3 | composer.phar 4 | composer.lock 5 | .php-cs-fixer.cache 6 | .phpunit.result.cache 7 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->notName('*.blade.php') 10 | ->ignoreDotFiles(true) 11 | ->ignoreVCS(true); 12 | 13 | return (new PhpCsFixer\Config()) 14 | ->setRules([ 15 | '@PSR12' => true, 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 18 | 'no_unused_imports' => true, 19 | 'not_operator_with_successor_space' => true, 20 | 'trailing_comma_in_multiline' => true, 21 | 'phpdoc_scalar' => true, 22 | 'unary_operator_spaces' => true, 23 | 'binary_operator_spaces' => true, 24 | 'blank_line_before_statement' => [ 25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 26 | ], 27 | 'phpdoc_single_line_var_spacing' => true, 28 | 'phpdoc_var_without_name' => true, 29 | 'class_attributes_separation' => [ 30 | 'elements' => [ 31 | 'method' => 'one', 32 | ], 33 | ], 34 | 'method_argument_space' => [ 35 | 'on_multiline' => 'ensure_fully_multiline', 36 | 'keep_multiple_spaces_after_comma' => true, 37 | ], 38 | 'single_trait_insert_per_statement' => true, 39 | ]) 40 | ->setFinder($finder); 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Juan Pablo 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 | # Api Resources 2 | [![Latest Version](https://img.shields.io/github/release/juampi92/api-resources.svg?style=flat-square)](https://github.com/juampi92/api-resources/releases) 3 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/juampi92/api-resources/run-tests.yml?branch=master&label=Tests&style=flat-square)](https://github.com/juampi92/api-resources/actions?query=workflow%3ATests+branch%3Amaster) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/juampi92/api-resources.svg?style=flat-square)](https://packagist.org/packages/juampi92/api-resources) 5 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 6 | 7 | Manage your resources maintaining API versioning. With a simple middleware separate routes by api version, and smart instanciate [Http\Resources](https://laravel.com/docs/5.5/eloquent-resources) based on this version. 8 | 9 | Add the middleware `'api.v:2'` on your api/v2 group. 10 | 11 | And then `api_resource('App\User')->make($user)` is the same as `new App\Http\Resources\App\v2\User($user)`, but version free. 12 | 13 | ```bash 14 | App\Http\Resources\ 15 | |- App\ 16 | |- v1\ 17 | |- User.php 18 | |- v2\ 19 | |- Rank.php 20 | |- User.php 21 | ``` 22 | 23 | ### The idea behing this 24 | 25 | A while back I faced this API versioning problem, so I wrote this [medium post](https://medium.com/@juampi92/api-versioning-using-laravels-resources-b1687a6d2c22) with my solution and this package reflects this. 26 | 27 | ## Installation 28 | 29 | You can install this package via composer using: 30 | 31 | ```bash 32 | composer require juampi92/api-resources 33 | ``` 34 | 35 | The package will automatically register itself. 36 | 37 | ### Config 38 | 39 | To publish the config file to `config/api.php` run: 40 | 41 | ```bash 42 | php artisan vendor:publish --provider="Juampi92\APIResources\APIResourcesServiceProvider" 43 | ``` 44 | 45 | This will publish a file `api.php` in your config directory with the following content: 46 | ```php 47 | return [ 48 | /* 49 | |-------------------------------------------------------------------------- 50 | | API Version 51 | |-------------------------------------------------------------------------- 52 | | 53 | | This value is the latest version of your api. This is used when 54 | | there's no specified version on the routes, so it will take this as the 55 | | default, or latest. 56 | */ 57 | 'version' => '1', 58 | 59 | /* 60 | |-------------------------------------------------------------------------- 61 | | Resources home path 62 | |-------------------------------------------------------------------------- 63 | | 64 | | This value is the base folder where your resources are stored. 65 | | When using multiple APIs, you can leave it as a string if every 66 | | api is in the same folder, or as an array with the APIs as keys. 67 | */ 68 | 'resources_path' => 'App\Http\Resources', 69 | 70 | /* 71 | |-------------------------------------------------------------------------- 72 | | Resources 73 | |-------------------------------------------------------------------------- 74 | | 75 | | Here is the folder that has versioned resources. If you store them 76 | | in the root of 'resources_path', leave this empty or null. 77 | */ 78 | 'resources' => 'App' 79 | ]; 80 | ``` 81 | 82 | ### Middleware 83 | 84 | Install this middleware on your `Http/Kernel.php` under the `$routeMiddleware` 85 | 86 | ```php 87 | protected $routeMiddleware = [ 88 | ... 89 | 'api.v' => \Juampi92\APIResources\Middleware\APIversion::class, 90 | ... 91 | ]; 92 | ``` 93 | 94 | ## Configure correctly 95 | 96 | For this package to work, you need to understand how it requires resources. 97 | 98 | If we have the following config: 99 | ```php 100 | [ 101 | 'version' => '2', 102 | 'resources_path' => 'App\Http\Resources', 103 | 'resources' => 'Api' 104 | ] 105 | ``` 106 | 107 | This means that if you include the `Api\User` resource, it will instantiate `App\Http\Resources\Api\v2\User`. 108 | 109 | `Api` works for sub organizing your structure, but you can put your Resources versionate folders in the root, like this: 110 | 111 | ```php 112 | [ 113 | 'version' => '2', 114 | 'resources_path' => 'App\Http\Resources', 115 | 'resources' => '' 116 | ] 117 | ``` 118 | 119 | Now if we include `User`, it will instantiate `App\Http\Resources\v2\User`. 120 | 121 | ### Fallback 122 | 123 | When you use a version that is **NOT** the latest, if you try to include a Resource that's **NOT** defined inside that version's directory, this will automatically fallback in the **LATEST** version. 124 | 125 | This way you don't have to duplicate new resources on previous versions. 126 | 127 | ## Usage 128 | 129 | ### Middleware 130 | 131 | When you group your API routes, you should now apply the middleware `api.v` into the group like this: 132 | 133 | ```php 134 | // App v1 API 135 | Route::group([ 136 | 'middleware' => ['app', 'api.v:1'], 137 | 'prefix' => 'api/v1', 138 | ], function ($router) { 139 | require base_path('routes/app_api.v1.php'); 140 | }); 141 | 142 | // App v2 API 143 | Route::group([ 144 | 'middleware' => ['app', 'api.v:2'], 145 | 'prefix' => 'api/v2', 146 | ], function ($router) { 147 | require base_path('routes/app_api.v2.php'); 148 | }); 149 | ``` 150 | 151 | That way, if you use the Facade, you can check the current version by doing `APIResource::getVersion()` and will return the version specified on the middleware. 152 | 153 | 154 | ### Facade 155 | 156 | There are many ways to create resources. You can use the Facade accessor: 157 | 158 | ```php 159 | use Juampi92\APIResources\Facades\APIResource; 160 | 161 | class SomethingController extends Controller { 162 | ... 163 | 164 | public function show(Something $model) 165 | { 166 | return APIResource::resolve('App\Something')->make($model); 167 | } 168 | } 169 | ``` 170 | 171 | ### Global helper 172 | 173 | ```php 174 | class SomethingController extends Controller { 175 | ... 176 | 177 | public function show(Something $model) 178 | { 179 | return api_resource('App\Something')->make($model); 180 | } 181 | } 182 | ``` 183 | 184 | ### Collections 185 | 186 | Instead of `make`, use `collection` for arrays, just like Laravel's documentation. 187 | 188 | ```php 189 | class SomethingController extends Controller { 190 | ... 191 | 192 | public function index() 193 | { 194 | $models = Something::all(); 195 | return api_resource('App\Something')->collection($models); 196 | } 197 | } 198 | ``` 199 | 200 | If you wanna use a ResourceCollection, you might wanna rewrite the `collects()` method. 201 | 202 | ```php 203 | class UserCollection extends ResourceCollection 204 | { 205 | protected function collects() 206 | { 207 | return APIResource::resolveClassname('App\User'); 208 | } 209 | } 210 | ``` 211 | 212 | This way, the ResourceCollection will always have the correct class. 213 | 214 | `resolveClassname` will try to use the current version of the class, but if it's not possible, will use the latest. 215 | 216 | ## Nested resources 217 | 218 | To take advantage of the **fallback** functionality, it's recomended to use `api_resource` inside the resources. This way you preserve the right version, or the latest if it's not defined. 219 | 220 | ```php 221 | class Post extends Resource { 222 | public function toArray($request) 223 | { 224 | return [ 225 | 'title' => $this->title, 226 | ... 227 | 'user' => api_resource('App\User')->make($this->user); 228 | ]; 229 | } 230 | } 231 | ``` 232 | 233 | ## Multiple APIs 234 | 235 | There might be the case where you have more than one API living on the same project, but using diferent versions. This app supports that. 236 | First, the `config/api.php` 237 | 238 | ```php 239 | return [ 240 | 'default' => 'api', 241 | 'version' => [ 242 | 'api' => '2', 243 | 'desktop' => '3' 244 | ], 245 | 'resources_path' => 'App\Http\Resources' 246 | // Or one path each 247 | 'resources_path' => [ 248 | 'api' => 'App\Http\Resources', 249 | 'desktop' => 'Vendorname\ExternalPackage\Resources' 250 | ], 251 | 'resources' => [ 252 | 'api' => 'Api', 253 | 'desktop' => '' 254 | ], 255 | ]; 256 | ``` 257 | 258 | Then, you need to configure the **middleware**. Instead of using `api.v:1`, you now have to specify the name: `api.v:3,desktop`. 259 | 260 | Then the rest works as explained before. 261 | 262 | 263 | ## API Route 264 | 265 | Sometimes you must return a route url on the api response. 266 | If you wanna keep the api version (which is always the current version), api-resources has the solution for you. 267 | 268 | ```php 269 | // When defining the routes 270 | Route::group([ 271 | 'middleware' => ['app', 'api.v:1'], 272 | 'prefix' => 'api/v1', 273 | // Using name on a group will prefix it. 274 | 'name' => 'api.v1.', 275 | ], function ($router) { 276 | Route::get('/auth/login', [ 277 | // This will be api.v1.auth.login 278 | 'name' => 'auth.login', 279 | 'use' => '...', 280 | ]); 281 | }); 282 | ``` 283 | 284 | With this we have `api.v1.auth.login` and `api.v2.auth.login` when creating a new version. 285 | 286 | Now just do `api_route('api.auth.login')`, and it will output `/api/v1/auth/login` or `/api/v2/auth/login` accordingly. 287 | 288 | ### How it works 289 | 290 | It's grabbing the config `api.resources` and doing a strtolower, so if you have `'resources' => 'App'`, will transform `app.auth.login` into `app.v1.auth.login`. 291 | If you need to customize it, add a new config entry in `config/api.php` like this: 292 | 293 | ```php 294 | /* 295 | |-------------------------------------------------------------------------- 296 | | Route prefix 297 | |-------------------------------------------------------------------------- 298 | | 299 | | By default, the route prefix is the lowercase resources folder. 300 | | So it'd be `app.v1.auth.login` has the prefix `app`. 301 | | 302 | | Using `app` will do api_route(`app.auth.login`) => `app.v?.auth.login`. 303 | | 304 | */ 305 | 306 | 'route_prefix' => 'app' 307 | ``` 308 | 309 | If works with multiple APIs as explained before. 310 | 311 | ## Testing 312 | 313 | Run the tests with: 314 | ```bash 315 | vendor/bin/phpunit 316 | ``` 317 | 318 | ## Credits 319 | 320 | - [Juan Pablo Barreto](https://github.com/juampi92) 321 | - [All Contributors](../../contributors) 322 | 323 | ## License 324 | 325 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 326 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "juampi92/api-resources", 3 | "description": "Manage your resources maintaining API versioning", 4 | "homepage": "https://github.com/juampi92/api-resources", 5 | "license": "MIT", 6 | "keywords": [ 7 | "laravel", 8 | "api" 9 | ], 10 | "authors": [ 11 | { 12 | "name": "juampi92", 13 | "email": "juampi92@gmail.com" 14 | } 15 | ], 16 | "require": { 17 | "php": "^7.4|^8.0|^8.1|^8.2", 18 | "illuminate/support": "^7.0|^8.0|^9.0|^10.0|^11.0", 19 | "illuminate/http": "^7.0|^8.0|^9.0|^10.0|^11.0" 20 | }, 21 | "require-dev": { 22 | "orchestra/testbench": "^5.0|^6.0|^7.0|^8.0|^9.0", 23 | "phpunit/phpunit": "^9.4|^10.5", 24 | "friendsofphp/php-cs-fixer": "^3.8", 25 | "phpstan/phpstan": "^1.9" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Juampi92\\APIResources\\": "src/" 30 | }, 31 | "files": [ 32 | "src/helpers.php" 33 | ] 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "Juampi92\\APIResources\\Tests\\": "tests/" 38 | } 39 | }, 40 | "scripts": { 41 | "test": "vendor/bin/phpunit --colors=always", 42 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage", 43 | "php-cs-fix": "vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php", 44 | "phpstan": "vendor/bin/phpstan analyse -c phpstan.neon" 45 | }, 46 | "extra": { 47 | "laravel": { 48 | "providers": [ 49 | "Juampi92\\APIResources\\APIResourcesServiceProvider" 50 | ], 51 | "aliases": { 52 | "APIResource": "Juampi92\\APIResources\\Facades\\APIResource" 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 8 3 | paths: 4 | - src 5 | ignoreErrors: 6 | - '#Parameter \#2 \$args of function forward_static_call_array expects array\, array\ given.#' 7 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | src/ 6 | 7 | 8 | 9 | 10 | tests 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /publishable/config/api.php: -------------------------------------------------------------------------------- 1 | '1', 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Resources home path 19 | |-------------------------------------------------------------------------- 20 | | 21 | | This value is the base folder where your resources are stored. 22 | | When using multiple APIs, you can leave it as a string if every 23 | | api is in the same folder, or as an array with the APIs as keys. 24 | */ 25 | 26 | 'resources_path' => 'App\Http\Resources', 27 | 28 | /* 29 | |-------------------------------------------------------------------------- 30 | | Resources 31 | |-------------------------------------------------------------------------- 32 | | 33 | | Here is the folder that has versioned resources. If you store them 34 | | in the root of 'resources_path', leave this empty or null. 35 | */ 36 | 37 | 'resources' => 'App', 38 | 39 | /* 40 | |-------------------------------------------------------------------------- 41 | | Route prefix 42 | |-------------------------------------------------------------------------- 43 | | 44 | | By default, the route prefix is the lowercase resources folder. 45 | | So it'd be `app.v1.auth.login` has the prefix `app`. 46 | | 47 | | Using `app` will do api_route(`app.auth.login`) => `app.v?.auth.login`. 48 | */ 49 | 50 | // 'route_prefix' => 'app' 51 | 52 | ]; 53 | -------------------------------------------------------------------------------- /src/APIResource.php: -------------------------------------------------------------------------------- 1 | */ 13 | protected $path; 14 | 15 | /** 16 | * @param class-string $path 17 | */ 18 | public function __construct($path) 19 | { 20 | $this->path = $path; 21 | } 22 | 23 | /** 24 | * @param mixed ...$args 25 | * 26 | * @return JsonResource 27 | */ 28 | public function with(...$args) 29 | { 30 | return forward_static_call_array([$this->path, 'make'], $args); 31 | } 32 | 33 | /** 34 | * @param mixed ...$args 35 | * 36 | * @return JsonResource 37 | */ 38 | public function make(...$args) 39 | { 40 | return forward_static_call_array([$this->path, 'make'], $args); 41 | } 42 | 43 | /** 44 | * @param mixed ...$args 45 | * 46 | * @return JsonResource 47 | */ 48 | public function collection(...$args) 49 | { 50 | return forward_static_call_array([$this->path, 'collection'], $args); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/APIResourceManager.php: -------------------------------------------------------------------------------- 1 | getConfig('version', $defaultName); 46 | 47 | if (! $latestVersion) { 48 | throw new Exception('You must define a config(\'api\') with a latest version. Do: php artisan vendor:publish --provider="Juampi92/APIResources/APIResourcesServiceProvider"'); 49 | } 50 | 51 | $this->latest = $latestVersion; 52 | $this->setVersion($latestVersion, $defaultName); 53 | } 54 | 55 | /** 56 | * Returns the name of the versioned route. 57 | * 58 | * @param string $route 59 | * @return string 60 | */ 61 | public function getRouteName($route) 62 | { 63 | if (! $this->routePath) { 64 | // Grab route_prefix config first. If it's not set, 65 | // grab the resources, and replace `\` with `.`, and 66 | // transform it all to lowercase. 67 | $this->routePath = $this->getConfig('route_prefix') 68 | ?: str_replace('\\', '.', strtolower($this->getConfig('resources'))); 69 | } 70 | 71 | return "{$this->routePath}.v{$this->current}" . Str::after($route, $this->routePath); 72 | } 73 | 74 | /** 75 | * Returns the versioned url. 76 | * 77 | * @param string $name 78 | * @param array<\Illuminate\Contracts\Routing\UrlRoutable|string|\BackedEnum> $parameters 79 | * @param bool $absolute 80 | * @return string 81 | */ 82 | public function getRoute($name, $parameters = [], $absolute = true) 83 | { 84 | return route($this->getRouteName($name), $parameters, $absolute); 85 | } 86 | 87 | /** 88 | * Get config considering the API name if present. 89 | * 90 | * @param string $cfg Config path 91 | * @param string $name Name of api if present 92 | * 93 | * @return mixed The result of the config 94 | */ 95 | protected function getConfig($cfg, $name = null) 96 | { 97 | if (is_null($name)) { 98 | $name = $this->apiName; 99 | } 100 | 101 | $name = $name ? ".$name" : ''; 102 | 103 | return config("api.$cfg{$name}"); 104 | } 105 | 106 | /** 107 | * Sets the current API version. 108 | * 109 | * @param string $current 110 | * @param string|null $apiName = null 111 | * 112 | * @return $this 113 | */ 114 | public function setVersion($current, $apiName = null) 115 | { 116 | // Reset pre-cached properties 117 | $this->current = $current; 118 | $this->apiName = $apiName; 119 | 120 | $this->routePath = null; 121 | $this->latest = $this->getConfig('version'); 122 | 123 | // Path can be only one or one for each api 124 | $this->path = config('api.resources_path'); 125 | if (is_array($this->path)) { 126 | $this->path = $this->getConfig('resources_path'); 127 | } 128 | 129 | $this->resources = $this->getConfig('resources'); 130 | 131 | return $this; 132 | } 133 | 134 | /** 135 | * Gets the current API version. 136 | * 137 | * @return string 138 | */ 139 | public function getVersion() 140 | { 141 | return $this->current; 142 | } 143 | 144 | /** 145 | * Checks if the given version is the latest. 146 | * 147 | * @param string $current 148 | * 149 | * @return bool 150 | */ 151 | public function isLatest($current = null) 152 | { 153 | if (! isset($current)) { 154 | $current = $this->current; 155 | } 156 | 157 | return $this->latest === $current; 158 | } 159 | 160 | /** 161 | * Returns the classname of the versioned resource, 162 | * or it's latest version if it doesn't exist. 163 | * 164 | * Throws an exception if it cannot find it. 165 | * 166 | * @param string $classname 167 | * 168 | * @return class-string 169 | * @throws ResourceNotFoundException 170 | */ 171 | public function resolveClassname($classname) 172 | { 173 | $path = $this->parseClassname($classname); 174 | 175 | // Check if the resource was found 176 | if (class_exists($path)) { 177 | return $path; 178 | } 179 | 180 | // If we are on the latest version, stop searching 181 | if ($this->isLatest()) { 182 | throw new Exceptions\ResourceNotFoundException($classname, $path); 183 | } 184 | 185 | // Search on the latest version 186 | $path = $this->parseClassname($classname, true); 187 | 188 | // If still does not exists, fail 189 | if (! class_exists($path)) { 190 | throw new Exceptions\ResourceNotFoundException($classname, $path); 191 | } 192 | 193 | return $path; 194 | } 195 | 196 | /** 197 | * Returns the classname with the version considering. 198 | * 199 | * @param string $classname 200 | * @param bool $forceLatest Set to true if last version is required 201 | * 202 | * @return class-string 203 | */ 204 | protected function parseClassname($classname, $forceLatest = false) 205 | { 206 | $version = $forceLatest ? $this->latest : $this->current; 207 | 208 | if (! empty($this->resources)) { 209 | $path = $this->resources . "\\v{$version}\\" . Str::after($classname, $this->resources . "\\"); 210 | } else { 211 | $path = "v{$version}\\" . $classname; 212 | } 213 | 214 | $path = "\\{$this->path}\\{$path}"; 215 | 216 | // @phpstan-ignore-next-line 217 | return $path; 218 | } 219 | 220 | /** 221 | * Smart builds the classname using the correct version. 222 | * If it fails with the current version, it falls back to 223 | * the latest version. If it still fails, throw exception. 224 | * 225 | * @param string $classname 226 | * 227 | * @return APIResource 228 | * @throws Exceptions\ResourceNotFoundException 229 | */ 230 | public function resolve($classname) 231 | { 232 | $path = $this->resolveClassname($classname); 233 | 234 | // Check if the resource was found 235 | if (! class_exists($path)) { 236 | // If we are on the latest version, stop searching 237 | if ($this->isLatest()) { 238 | throw new Exceptions\ResourceNotFoundException($classname, $path); 239 | } 240 | 241 | // Search on the latest version 242 | $path = $this->resolveClassname($classname); 243 | 244 | // If still does not exist, fail 245 | if (! class_exists($path)) { 246 | throw new Exceptions\ResourceNotFoundException($classname, $path); 247 | } 248 | } 249 | 250 | return new APIResource($path); 251 | } 252 | 253 | /** 254 | * @param string $classname 255 | * @param mixed ...$args 256 | * 257 | * @return JsonResource 258 | */ 259 | public function make($classname, ...$args) 260 | { 261 | $resource = $this->resolve($classname); 262 | 263 | return $resource->make(...$args); 264 | } 265 | 266 | /** 267 | * @param string $classname 268 | * @param mixed ...$args 269 | * 270 | * @return JsonResource 271 | */ 272 | public function collection($classname, ...$args) 273 | { 274 | $resource = $this->resolve($classname); 275 | 276 | return $resource->collection(...$args); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/APIResourcesServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 17 | $this->registerPublishables(); 18 | } 19 | } 20 | 21 | /** 22 | * Register the service provider. 23 | * 24 | * @return void 25 | */ 26 | public function register() 27 | { 28 | $this->app->singleton('apiresource', function () { 29 | return new APIResourceManager(); 30 | }); 31 | } 32 | 33 | /** 34 | * Registers the publishable config. 35 | * 36 | * @return void 37 | */ 38 | protected function registerPublishables() 39 | { 40 | $this->publishes([ 41 | __DIR__ . '/../publishable/config/api.php' => config_path('api.php'), 42 | ]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Exceptions/APIDeprecatedException.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | function api_resource($classname) 15 | { 16 | return APIResource::resolve($classname); 17 | } 18 | } 19 | 20 | if (! function_exists('api_route')) { 21 | /** 22 | * Generate the URL to a versioned named route. 23 | * 24 | * @param string $name 25 | * @param mixed $parameters 26 | * @param bool $absolute 27 | * @return string 28 | */ 29 | function api_route($name, $parameters = [], $absolute = true) 30 | { 31 | return APIResource::getRoute($name, $parameters, $absolute); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/APIConfigTest.php: -------------------------------------------------------------------------------- 1 | require __DIR__ . '/../publishable/config/api.php']); 15 | } 16 | 17 | public function test_it_can_get_nested_routes() 18 | { 19 | config(['api.resource' => [ 20 | 'app' => $appResource = 'App', 21 | 'api' => $apiResource = 'API', 22 | ]]); 23 | $resourceManager = new APIResourceManager(); 24 | $resourceManager->setVersion('1', 'app'); 25 | 26 | $config = $this->callMethod($resourceManager, 'getConfig', ['resource']); 27 | $this->assertEquals($appResource, $config); 28 | 29 | $config = $this->callMethod($resourceManager, 'getConfig', ['resource', 'api']); 30 | $this->assertEquals($apiResource, $config); 31 | } 32 | 33 | public function test_it_can_get_non_nested_routes() 34 | { 35 | config(['api.resource' => 'WebApp']); 36 | $resourceManager = new APIResourceManager(); 37 | $resourceManager->setVersion('2'); 38 | 39 | $config = $this->callMethod($resourceManager, 'getConfig', ['resource']); 40 | $this->assertEquals('WebApp', $config); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/APIResourceCollectionTest.php: -------------------------------------------------------------------------------- 1 | require __DIR__ . '/Fixtures/config/simple.php']); 15 | } 16 | 17 | public function test_simple_resource_with_collection() 18 | { 19 | $users = collect([new Fixtures\Models\User(), new Fixtures\Models\User()]); 20 | 21 | APIResourceFacade::setVersion('2'); 22 | $resource = api_resource('App\User')->collection($users); 23 | 24 | $this->assertResourceArray($resource, ['data' => [ 25 | [ 26 | 'id' => 1, 27 | 'name' => 'asd', 28 | 'rank' => [ 29 | 'id' => 1, 30 | 'name' => 'adm', 31 | 'v' => 2, 32 | ], 33 | 'v' => 2, 34 | ], [ 35 | 'id' => 1, 36 | 'name' => 'asd', 37 | 'rank' => [ 38 | 'id' => 1, 39 | 'name' => 'adm', 40 | 'v' => 2, 41 | ], 42 | 'v' => 2, 43 | ], 44 | ]]); 45 | } 46 | 47 | public function test_collection_resource() 48 | { 49 | $users = collect([new Fixtures\Models\User(), new Fixtures\Models\User()]); 50 | 51 | APIResourceFacade::setVersion('2'); 52 | 53 | $asd = 'random'; 54 | 55 | $resource = api_resource('App\Users') 56 | ->make($users) 57 | ->setAsd($asd); 58 | 59 | $this->assertResourceArray($resource, ['data' => [ 60 | [ 61 | 'id' => 1, 62 | 'name' => 'asd', 63 | 'rank' => [ 64 | 'id' => 1, 65 | 'name' => 'adm', 66 | 'v' => 2, 67 | ], 68 | 'v' => 2, 69 | ], [ 70 | 'id' => 1, 71 | 'name' => 'asd', 72 | 'rank' => [ 73 | 'id' => 1, 74 | 'name' => 'adm', 75 | 'v' => 2, 76 | ], 77 | 'v' => 2, 78 | ], 79 | ], 80 | 'asd' => $asd, 81 | ]); 82 | } 83 | 84 | public function test_versioned_collection() 85 | { 86 | config(['api' => require __DIR__ . '/Fixtures/config/multi.php']); 87 | $users = collect([new Fixtures\Models\User(), new Fixtures\Models\User()]); 88 | 89 | APIResourceFacade::setVersion('1', 'collection'); 90 | 91 | $resource = api_resource('Collections\UserCollection') 92 | ->make($users); 93 | 94 | $this->assertResourceArray($resource, ['data' => [ 95 | [ 96 | 'id' => 1, 97 | 'name' => 'asd', 98 | 'v' => 1, 99 | ], [ 100 | 'id' => 1, 101 | 'name' => 'asd', 102 | 'v' => 1, 103 | ], 104 | ]]); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/APIResourceTest.php: -------------------------------------------------------------------------------- 1 | require __DIR__ . '/Fixtures/config/simple.php']); 18 | } 19 | 20 | public function test_manager_resolve_returns_a_resource() 21 | { 22 | APIResourceFacade::setVersion(config('app.version')); 23 | 24 | $object = APIResourceFacade::resolve('App\User'); 25 | $this->assertInstanceOf(APIResource::class, $object); 26 | } 27 | 28 | public function _test_make_returns_a_class() 29 | { 30 | APIResourceFacade::setVersion(config('api.version')); 31 | 32 | APIResourceFacade::make('App\User', ['a' => 'b']); 33 | } 34 | 35 | public function test_api_resource_helper_returns_an_instance() 36 | { 37 | APIResourceFacade::setVersion('2'); 38 | 39 | $resource = api_resource('App\User'); 40 | $this->assertInstanceOf(APIResource::class, $resource); 41 | $this->assertAttributeEquals('\Juampi92\APIResources\Tests\Fixtures\Resources\App\v2\User', 'path', $resource); 42 | 43 | //$this->assertInstanceOf(APIResource::class, $object); 44 | //APIResourceFacade::make('App\User', [ 'a' => 'b']); 45 | } 46 | 47 | public function test_returns_resource() 48 | { 49 | $user = new Fixtures\Models\User(); 50 | 51 | APIResourceFacade::setVersion('2'); 52 | $resource = api_resource('App\User')->make($user); 53 | 54 | $this->assertInstanceOf(Fixtures\Resources\App\v2\User::class, $resource); 55 | 56 | $this->assertResourceArray($resource, ['data' => [ 57 | 'id' => 1, 58 | 'name' => 'asd', 59 | 'rank' => [ 60 | 'id' => 1, 61 | 'name' => 'adm', 62 | 'v' => 2, 63 | ], 64 | 'v' => 2, 65 | ]]); 66 | } 67 | 68 | public function test_fallback_to_latest_version() 69 | { 70 | // Set latest as 2 71 | config(['api.version' => 2]); 72 | $resourceManager = new APIResourceManager(); 73 | 74 | $resourceManager->setVersion('1'); 75 | 76 | $this->assertAttributeEquals(2, 'latest', $resourceManager); 77 | $this->assertAttributeEquals(1, 'current', $resourceManager); 78 | 79 | $resource = $resourceManager->resolve('App\Post'); 80 | $this->assertAttributeEquals('\Juampi92\APIResources\Tests\Fixtures\Resources\App\v2\Post', 'path', $resource); 81 | } 82 | 83 | public function test_fails_if_no_fallback() 84 | { 85 | $this->expectException(ResourceNotFoundException::class); 86 | 87 | // Set latest as 2 88 | config(['api.version' => 2]); 89 | $resourceManager = new APIResourceManager(); 90 | 91 | $resource = $resourceManager->resolve('App\Comment'); 92 | } 93 | 94 | public function test_nested_resources_simple() 95 | { 96 | $user = new Fixtures\Models\User(); 97 | 98 | APIResourceFacade::setVersion('2'); 99 | $resource = api_resource('App\User')->make($user); 100 | 101 | $this->assertInstanceOf(Fixtures\Resources\App\v2\User::class, $resource); 102 | 103 | $this->assertResourceArray($resource, ['data' => [ 104 | 'id' => 1, 105 | 'name' => 'asd', 106 | 'rank' => [ 107 | 'id' => 1, 108 | 'name' => 'adm', 109 | 'v' => 2, 110 | ], 111 | 'v' => 2, 112 | ]]); 113 | } 114 | 115 | public function test_nested_resources_with_fallback() 116 | { 117 | config(['api.version' => 2]); 118 | $resourceManager = new APIResourceManager(); 119 | 120 | $user = new Fixtures\Models\User(); 121 | 122 | $resourceManager->setVersion('1'); 123 | $resource = $resourceManager->resolve('App\User')->make($user); 124 | 125 | $this->assertInstanceOf(Fixtures\Resources\App\v1\User::class, $resource); 126 | 127 | $this->assertResourceArray($resource, ['data' => [ 128 | 'id' => 1, 129 | 'name' => 'asd', 130 | 'rank' => [ 131 | 'id' => 1, 132 | 'name' => 'adm', 133 | 'v' => 2, 134 | ], 135 | 'v' => 1, 136 | ]]); 137 | } 138 | 139 | public function test_without_resource_folder() 140 | { 141 | config(['api.resources' => '']); 142 | $resourceManager = new APIResourceManager(); 143 | 144 | $user = new Fixtures\Models\User(); 145 | 146 | $resourceManager->setVersion('1'); 147 | $resource = $resourceManager->resolve('User')->make($user); 148 | 149 | $this->assertInstanceOf(Fixtures\Resources\v1\User::class, $resource); 150 | 151 | $this->assertResourceArray($resource, ['data' => [ 152 | 'id' => 1, 153 | 'name' => 'asd', 154 | 'v' => 1, 155 | ]]); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /tests/APIResourcesMultipleTest.php: -------------------------------------------------------------------------------- 1 | require __DIR__ . '/Fixtures/config/multi.php']); 15 | } 16 | 17 | public function test_nested_resources_with_fallback() 18 | { 19 | config([ 20 | 'api.version' => [ 21 | 'app' => '2', 22 | 'desktop' => '1', 23 | ], 24 | 'api.default' => 'app', 25 | ]); 26 | $resourceManager = new APIResourceManager(); 27 | 28 | $user = new Fixtures\Models\User(); 29 | 30 | $resourceManager->setVersion('1', 'app'); 31 | $resource = $resourceManager->resolve('App\User')->make($user); 32 | 33 | $this->assertInstanceOf(Fixtures\Resources\App\v1\User::class, $resource); 34 | 35 | /* 36 | * Now change to the desktop API 37 | */ 38 | 39 | $resourceManager->setVersion('2', 'desktop'); 40 | $resource = $resourceManager->resolve('Api\User')->make($user); 41 | 42 | $this->assertInstanceOf(Fixtures\Resources\Api\v2\User::class, $resource); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/APIRouteTest.php: -------------------------------------------------------------------------------- 1 | require __DIR__ . '/../publishable/config/api.php']); 15 | } 16 | 17 | public function test_it_can_get_default_route_prefix() 18 | { 19 | config(['api.resources' => 'App']); 20 | $resourceManager = new APIResourceManager(); 21 | $resourceManager->setVersion('1'); 22 | 23 | $this->assertEquals('app.v1.auth.login', $resourceManager->getRouteName('app.auth.login')); 24 | } 25 | 26 | public function test_it_can_get_custom_route_prefix() 27 | { 28 | config(['api.resources' => 'Api', 'api.route_prefix' => 'appp']); 29 | $resourceManager = new APIResourceManager(); 30 | $resourceManager->setVersion('1'); 31 | 32 | $this->assertEquals('appp.v1.auth.login', $resourceManager->getRouteName('appp.auth.login')); 33 | } 34 | 35 | public function test_it_can_get_multiple_resources() 36 | { 37 | config(['api.resources' => [ 38 | 'app' => 'App2', 39 | 'default' => 'Apii', 40 | ]]); 41 | $resourceManager = new APIResourceManager(); 42 | 43 | $resourceManager->setVersion('1', 'default'); 44 | $this->assertEquals('apii.v1.auth.login', $resourceManager->getRouteName('apii.auth.login')); 45 | 46 | $resourceManager->setVersion('1', 'app'); 47 | $this->assertEquals('app2.v1.auth.login', $resourceManager->getRouteName('app2.auth.login')); 48 | } 49 | 50 | public function test_it_can_get_multiple_route_paths() 51 | { 52 | config([ 53 | 'api.route_prefix' => [ 54 | 'app' => 'this_is_custom', 55 | 'default' => 'wow', 56 | ], 57 | 'api.resources' => [ 58 | 'app' => 'App2', 59 | 'default' => 'Apii', 60 | ], ]); 61 | $resourceManager = new APIResourceManager(); 62 | 63 | $resourceManager->setVersion('1', 'app'); 64 | $this->assertEquals('this_is_custom.v1.auth.login', $resourceManager->getRouteName('this_is_custom.auth.login')); 65 | 66 | $resourceManager->setVersion('1', 'default'); 67 | $this->assertEquals('wow.v1.auth.login', $resourceManager->getRouteName('wow.auth.login')); 68 | } 69 | 70 | public function test_it_can_get_subdomains() 71 | { 72 | config([ 73 | 'api.route_prefix' => 'app.api', 74 | 'api.resources' => 'App\API', 75 | ]); 76 | $resourceManager = new APIResourceManager(); 77 | 78 | $resourceManager->setVersion('1'); 79 | $this->assertEquals('app.api.v1.auth.login', $resourceManager->getRouteName('app.api.auth.login')); 80 | } 81 | 82 | public function test_it_can_get_subdomains_by_default() 83 | { 84 | config([ 85 | 'api.resources' => 'App\API\Web', 86 | ]); 87 | $resourceManager = new APIResourceManager(); 88 | 89 | $resourceManager->setVersion('1'); 90 | $this->assertEquals('app.api.web.v1.auth.login', $resourceManager->getRouteName('app.api.web.auth.login')); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/BasicTest.php: -------------------------------------------------------------------------------- 1 | require __DIR__ . '/Fixtures/config/simple.php']); 16 | } 17 | 18 | public function test_it_can_construct() 19 | { 20 | $object = new APIResourceManager(); 21 | $this->assertInstanceOf(APIResourceManager::class, $object); 22 | } 23 | 24 | public function test_it_defaults_correctly() 25 | { 26 | config(['api' => [ 27 | 'version' => '2', 28 | 'resources_path' => 'App\Resources', 29 | 'resources' => 'Api', 30 | ], 31 | ]); 32 | $object = new APIResourceManager(); 33 | $this->assertAttributeEquals('2', 'current', $object); 34 | $this->assertAttributeEquals('App\Resources', 'path', $object); 35 | $this->assertAttributeEquals('Api', 'resources', $object); 36 | 37 | config(['api' => [ 38 | 'version' => '1', 39 | 'resources_path' => 'App\Resources2', 40 | 'resources' => 'Api2', 41 | ], 42 | ]); 43 | $object->setVersion('2'); 44 | $this->assertAttributeEquals('2', 'current', $object); 45 | $this->assertAttributeEquals('App\Resources2', 'path', $object); 46 | $this->assertAttributeEquals('Api2', 'resources', $object); 47 | } 48 | 49 | public function test_it_can_facade() 50 | { 51 | APIResourceFacade::setVersion('2'); 52 | $this->assertEquals('2', APIResourceFacade::getVersion()); 53 | 54 | APIResourceFacade::setVersion('5'); 55 | $this->assertEquals('5', APIResourceFacade::getVersion()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Fixtures/Arrayable.php: -------------------------------------------------------------------------------- 1 | 2, 18 | 'title' => 'asdasd', 19 | 'body' => 'Lorem Ipsum', 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Fixtures/Models/Rank.php: -------------------------------------------------------------------------------- 1 | 1, 17 | 'name' => 'adm', 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Fixtures/Models/User.php: -------------------------------------------------------------------------------- 1 | 1, 29 | 'name' => 'asd', 30 | 'posts' => [ 31 | new Post(), 32 | ], 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Fixtures/Resources/Api/v1/Posts/Single.php: -------------------------------------------------------------------------------- 1 | $this->id, 14 | 'title' => $this->title, 15 | ]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Fixtures/Resources/Api/v1/User.php: -------------------------------------------------------------------------------- 1 | $this->id, 14 | 'name' => $this->name, 15 | 'rank' => api_resource('App\Rank')->make($this->rank()), 16 | 'v' => 1, 17 | ]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Fixtures/Resources/Api/v2/Post.php: -------------------------------------------------------------------------------- 1 | $this->id, 14 | 'title' => $this->title, 15 | ]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Fixtures/Resources/Api/v2/Rank.php: -------------------------------------------------------------------------------- 1 | $this->id, 14 | 'name' => $this->name, 15 | 'v' => 2, 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Fixtures/Resources/Api/v2/User.php: -------------------------------------------------------------------------------- 1 | $this->id, 14 | 'name' => $this->name, 15 | 'rank' => api_resource('App\Rank') 16 | ->make($this->rank()), 17 | 'v' => 2, 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Fixtures/Resources/App/v1/Posts/Single.php: -------------------------------------------------------------------------------- 1 | $this->id, 14 | 'title' => $this->title, 15 | ]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Fixtures/Resources/App/v1/User.php: -------------------------------------------------------------------------------- 1 | $this->id, 14 | 'name' => $this->name, 15 | 'rank' => api_resource('App\Rank')->make($this->rank()), 16 | 'v' => 1, 17 | ]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Fixtures/Resources/App/v2/Post.php: -------------------------------------------------------------------------------- 1 | $this->id, 14 | 'title' => $this->title, 15 | ]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Fixtures/Resources/App/v2/Rank.php: -------------------------------------------------------------------------------- 1 | $this->id, 14 | 'name' => $this->name, 15 | 'v' => 2, 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Fixtures/Resources/App/v2/User.php: -------------------------------------------------------------------------------- 1 | $this->id, 14 | 'name' => $this->name, 15 | 'rank' => api_resource('App\Rank') 16 | ->make($this->rank()), 17 | 'v' => 2, 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Fixtures/Resources/App/v2/Users.php: -------------------------------------------------------------------------------- 1 | asd = $val; 14 | 15 | return $this; 16 | } 17 | 18 | public function toArray($request) 19 | { 20 | return [ 21 | 'data' => api_resource('App\User')->collection($this->collection), 22 | 'asd' => $this->asd, 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Fixtures/Resources/Collections/v1/User.php: -------------------------------------------------------------------------------- 1 | $this->id, 14 | 'name' => $this->name, 15 | 'v' => 1, 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Fixtures/Resources/Collections/v2/User.php: -------------------------------------------------------------------------------- 1 | $this->id, 14 | 'name' => $this->name, 15 | 'v' => 2, 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Fixtures/Resources/Collections/v2/UserCollection.php: -------------------------------------------------------------------------------- 1 | $this->id, 14 | 'name' => $this->name, 15 | 'v' => 1, 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Fixtures/config/multi.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'app' => '1', 16 | 'desktop' => '2', 17 | 'collection' => '2', 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | API Default 23 | |-------------------------------------------------------------------------- 24 | | 25 | | 26 | */ 27 | 28 | 'default' => 'app', 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Resorces homepath 33 | |-------------------------------------------------------------------------- 34 | | 35 | | This value is the base folder where your resources are stored. 36 | | 37 | */ 38 | 39 | 'resources_path' => 'Juampi92\APIResources\Tests\Fixtures\Resources', 40 | 41 | /* 42 | |-------------------------------------------------------------------------- 43 | | Resorces 44 | |-------------------------------------------------------------------------- 45 | | 46 | | Here is the folder that has versionated resources. If you store them 47 | | in the root, leave this empty '' 48 | */ 49 | 50 | 'resources' => [ 51 | 'app' => 'App', 52 | 'desktop' => 'Api', 53 | 'collection' => 'Collections', 54 | ], 55 | 56 | ]; 57 | -------------------------------------------------------------------------------- /tests/Fixtures/config/simple.php: -------------------------------------------------------------------------------- 1 | '1', 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Resorces homepath 19 | |-------------------------------------------------------------------------- 20 | | 21 | | This value is the base folder where your resources are stored. 22 | | 23 | */ 24 | 25 | 'resources_path' => 'Juampi92\APIResources\Tests\Fixtures\Resources', 26 | 27 | /* 28 | |-------------------------------------------------------------------------- 29 | | Resorces 30 | |-------------------------------------------------------------------------- 31 | | 32 | | Here is the folder that has versionated resources. If you store them 33 | | in the root, leave this empty '' 34 | */ 35 | 36 | 'resources' => 'App', 37 | 38 | ]; 39 | -------------------------------------------------------------------------------- /tests/ResourcePathResolveTest.php: -------------------------------------------------------------------------------- 1 | require __DIR__ . '/../publishable/config/api.php']); 20 | 21 | $this->apiResourceManager = new APIResourceManager(); 22 | } 23 | 24 | public function test_it_can_resolve_api_changes() 25 | { 26 | $this->apiResourceManager->setVersion('1'); 27 | $classname = $this->callMethod($this->apiResourceManager, 'parseClassname', ['App\User']); 28 | $this->assertEquals('\\App\\Http\\Resources\\App\\v1\\User', $classname); 29 | 30 | $this->apiResourceManager->setVersion('2'); 31 | $classname = $this->callMethod($this->apiResourceManager, 'parseClassname', ['App\Users']); 32 | $this->assertEquals('\\App\\Http\\Resources\\App\\v2\\Users', $classname); 33 | 34 | $this->apiResourceManager->setVersion('3'); 35 | $classname = $this->callMethod($this->apiResourceManager, 'parseClassname', ['App\User\Single']); 36 | $this->assertEquals('\\App\\Http\\Resources\\App\\v3\\User\\Single', $classname); 37 | } 38 | 39 | public function test_it_can_resolve_resource_path_changes() 40 | { 41 | config(['api.resources_path' => 'App\Resources']); 42 | $this->apiResourceManager->setVersion('3'); 43 | $classname = $this->callMethod($this->apiResourceManager, 'parseClassname', ['App\User']); 44 | $this->assertEquals('\\App\\Resources\\App\\v3\\User', $classname); 45 | 46 | config(['api.resources_path' => 'App\Resources2']); 47 | $this->apiResourceManager->setVersion('4'); 48 | $classname = $this->callMethod($this->apiResourceManager, 'parseClassname', ['App\User']); 49 | $this->assertEquals('\\App\\Resources2\\App\\v4\\User', $classname); 50 | } 51 | 52 | public function test_it_can_resolve_resources_prefix_changes() 53 | { 54 | config(['api.resources' => 'Api']); 55 | $this->apiResourceManager->setVersion('1'); 56 | $classname = $this->callMethod($this->apiResourceManager, 'parseClassname', ['Api\User']); 57 | $this->assertEquals('\\App\\Http\\Resources\\Api\\v1\\User', $classname); 58 | 59 | config(['api.resources' => 'Api\App']); 60 | $this->apiResourceManager->setVersion('1'); 61 | $classname = $this->callMethod($this->apiResourceManager, 'parseClassname', ['Api\App\User']); 62 | $this->assertEquals('\\App\\Http\\Resources\\Api\\App\\v1\\User', $classname); 63 | } 64 | 65 | public function test_it_can_resolve_resources_prefix_empty() 66 | { 67 | config(['api.resources' => '']); 68 | $this->apiResourceManager->setVersion('1'); 69 | $classname = $this->callMethod($this->apiResourceManager, 'parseClassname', ['User']); 70 | $this->assertEquals('\\App\\Http\\Resources\\v1\\User', $classname); 71 | 72 | config(['api.resources' => null]); 73 | $this->apiResourceManager->setVersion('1'); 74 | $classname = $this->callMethod($this->apiResourceManager, 'parseClassname', ['User']); 75 | $this->assertEquals('\\App\\Http\\Resources\\v1\\User', $classname); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | assertAttributeEquals($arr, 'data', $resource->response()); 24 | } 25 | 26 | public function callMethod($obj, $name, array $args) 27 | { 28 | $class = new \ReflectionClass($obj); 29 | $method = $class->getMethod($name); 30 | $method->setAccessible(true); 31 | 32 | return $method->invokeArgs($obj, $args); 33 | } 34 | 35 | protected function assertAttributeEquals($expects, $attribute, $object): void 36 | { 37 | $class = new ReflectionObject($object); 38 | $property = $class->getProperty($attribute); 39 | $property->setAccessible(true); 40 | 41 | $this->assertEquals($expects, $property->getValue($object)); 42 | } 43 | } 44 | --------------------------------------------------------------------------------