├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ ├── CS.yml │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── config └── api-postman.php ├── examples ├── 2021_02_04_151948_postman.json ├── 2021_02_04_155327_postman.json └── api.php ├── header.png ├── phpunit.xml ├── phpunit.xml.bak ├── src ├── Authentication │ ├── AuthenticationMethod.php │ ├── Basic.php │ └── Bearer.php ├── Commands │ └── ExportPostmanCommand.php ├── Concerns │ └── HasAuthentication.php ├── Exporter.php ├── PostmanGeneratorServiceProvider.php └── Processors │ ├── DocBlockProcessor.php │ ├── FormDataProcessor.php │ └── RouteProcessor.php └── tests ├── Feature ├── ExportPostmanTest.php └── ExportPostmanWithCacheTest.php ├── Fixtures ├── AuditLogController.php ├── CollectionHelpersTrait.php ├── ExampleController.php ├── ExampleEvent.js ├── ExampleFormRequest.php ├── ExampleModel.php ├── ExampleService.php └── UppercaseRule.php ├── TestCase.php └── Unit └── .gitkeep /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [andreaselia,tomirons] 2 | -------------------------------------------------------------------------------- /.github/workflows/CS.yml: -------------------------------------------------------------------------------- 1 | name: CS 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | tests: 8 | runs-on: ubuntu-latest 9 | name: Code Style 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Install Dependencies 13 | run: php /usr/bin/composer install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist 14 | - name: Verify 15 | run: php vendor/bin/pint --test 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 0 * * *' 8 | 9 | jobs: 10 | tests: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: true 14 | matrix: 15 | testbench: ['^8.0'] 16 | name: Testbench ${{ matrix.testbench }} 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Install Dependencies 20 | run: php /usr/bin/composer install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist 21 | - name: Install Testbench 22 | run: php /usr/bin/composer require -q --dev -W --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist orchestra/testbench:${{ matrix.testbench }} 23 | - name: Execute tests via PHPUnit 24 | run: php vendor/bin/phpunit --stop-on-failure 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /vendor 3 | composer.lock 4 | .phpunit.result.cache 5 | .phpunit.cache/test-results 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Andreas Elia 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 | ![Laravel API to Postman Header](/header.png) 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/andreaselia/laravel-api-to-postman/v)](//packagist.org/packages/andreaselia/laravel-api-to-postman) 4 | [![StyleCI](https://github.styleci.io/repos/323709695/shield?branch=master)](https://github.styleci.io/repos/323709695?branch=master) 5 | 6 | # Laravel API to Postman 7 | 8 | This package allows you to automatically generate a Postman collection based on your API routes. It also provides basic configuration and support for bearer auth tokens and basic auth for routes behind an auth middleware. 9 | 10 | For ```POST``` and ```PUT``` requests that utilizes a FormRequest, you can optionally scaffold the request, and publish rules in raw or human readable format. 11 | ## Postman Schema 12 | 13 | The generator works for the latest version of the Postman Schema at the time of publication (v2.1.0). 14 | 15 | ## Installation 16 | 17 | Install the package: 18 | 19 | ```bash 20 | composer require andreaselia/laravel-api-to-postman 21 | ``` 22 | 23 | Publish the config file: 24 | 25 | ```bash 26 | php artisan vendor:publish --provider="AndreasElia\PostmanGenerator\PostmanGeneratorServiceProvider" 27 | ``` 28 | 29 | ## Configuration 30 | 31 | You can modify any of the `api-postman.php` config values to suit your export requirements. 32 | 33 | Click [here](/config/api-postman.php) to view the config attributes. 34 | 35 | ## Usage 36 | 37 | The output of the command being ran is your storage/app directory. 38 | 39 | To use the command simply run: 40 | 41 | ```bash 42 | php artisan export:postman 43 | ``` 44 | 45 | The following usage will generate routes with the bearer token specified. 46 | 47 | ```bash 48 | php artisan export:postman --bearer="1|XXNKXXqJjfzG8XXSvXX1Q4pxxnkXmp8tT8TXXKXX" 49 | ``` 50 | 51 | The following usage will generate routes with the basic auth specified. 52 | 53 | ```bash 54 | php artisan export:postman --basic="username:password123" 55 | ``` 56 | 57 | If both auths are specified, bearer will be favored. 58 | 59 | ## Examples 60 | 61 | This is with the default configuration and a bearer token passed in: 62 | 63 | ```bash 64 | php artisan export:postman --bearer=123456789 65 | ``` 66 | 67 | - [Example routes](/examples/api.php) 68 | - [Example default output](/examples/2021_02_04_151948_postman.json) 69 | - [Example structured output](/examples/2021_02_04_155327_postman.json) 70 | 71 | ## Contributing 72 | 73 | You're more than welcome to submit a pull request, or if you're not feeling up to it - create an issue so someone else can pick it up. 74 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "andreaselia/laravel-api-to-postman", 3 | "description": "Generate a Postman collection automatically from your Laravel API", 4 | "keywords": [ 5 | "laravel", 6 | "postman", 7 | "api", 8 | "collection", 9 | "generate" 10 | ], 11 | "type": "library", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Andreas Elia", 16 | "email": "andreaselia@live.co.uk" 17 | } 18 | ], 19 | "autoload": { 20 | "psr-4": { 21 | "AndreasElia\\PostmanGenerator\\": "src/" 22 | } 23 | }, 24 | "autoload-dev": { 25 | "psr-4": { 26 | "AndreasElia\\PostmanGenerator\\Tests\\": "tests" 27 | } 28 | }, 29 | "require": { 30 | "php": "^7.4|^8.0", 31 | "ext-json": "*", 32 | "illuminate/config": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 33 | "illuminate/console": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 34 | "illuminate/contracts": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 35 | "illuminate/support": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 36 | "illuminate/routing": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 37 | "phpstan/phpdoc-parser": "^1.26" 38 | }, 39 | "extra": { 40 | "laravel": { 41 | "providers": [ 42 | "AndreasElia\\PostmanGenerator\\PostmanGeneratorServiceProvider" 43 | ] 44 | } 45 | }, 46 | "minimum-stability": "dev", 47 | "prefer-stable": true, 48 | "require-dev": { 49 | "orchestra/testbench": "^8.0|^9.0", 50 | "phpunit/phpunit": "^10.0|^11.0", 51 | "laravel/pint": "^1.13" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /config/api-postman.php: -------------------------------------------------------------------------------- 1 | env('APP_URL', 'http://localhost'), 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Collection Filename 19 | |-------------------------------------------------------------------------- 20 | | 21 | | The name for the collection file to be saved. 22 | | 23 | */ 24 | 25 | 'filename' => '{timestamp}_{app}_collection.json', 26 | 27 | /* 28 | |-------------------------------------------------------------------------- 29 | | Structured 30 | |-------------------------------------------------------------------------- 31 | | 32 | | If you want folders to be generated based on namespace. 33 | | 34 | | Set "crud_folders" to "false" if you don't want the api, index, store, show etc. folders. 35 | | 36 | */ 37 | 38 | 'structured' => false, 39 | 'crud_folders' => true, 40 | 41 | /* 42 | |-------------------------------------------------------------------------- 43 | | Auth Middleware 44 | |-------------------------------------------------------------------------- 45 | | 46 | | The middleware which wraps your authenticated API routes. 47 | | 48 | | E.g. auth:api, auth:sanctum 49 | | 50 | */ 51 | 52 | 'auth_middleware' => 'auth:api', 53 | 54 | /* 55 | |-------------------------------------------------------------------------- 56 | | Headers 57 | |-------------------------------------------------------------------------- 58 | | 59 | | The headers applied to all routes within the collection. 60 | | 61 | */ 62 | 63 | 'headers' => [ 64 | [ 65 | 'key' => 'Accept', 66 | 'value' => 'application/json', 67 | ], 68 | [ 69 | 'key' => 'Content-Type', 70 | 'value' => 'application/json', 71 | ], 72 | ], 73 | 74 | /* 75 | |-------------------------------------------------------------------------- 76 | | Events 77 | |-------------------------------------------------------------------------- 78 | | 79 | | If you want to configure the prequest and test scripts for the collection, 80 | | then please provide paths to the JavaScript files. 81 | | 82 | */ 83 | 84 | 'prerequest_script' => '', // This script will execute before every request in the collection. 85 | 'test_script' => '', // This script will execute after every request in the collection. 86 | 87 | /* 88 | |-------------------------------------------------------------------------- 89 | | Include Doc Comments 90 | |-------------------------------------------------------------------------- 91 | | 92 | | Determines whether to set the PHP Doc comments to the description 93 | | in postman. 94 | | 95 | */ 96 | 97 | 'include_doc_comments' => false, 98 | 99 | /* 100 | |-------------------------------------------------------------------------- 101 | | Enable Form Data 102 | |-------------------------------------------------------------------------- 103 | | 104 | | Determines whether or not form data should be handled. 105 | | 106 | */ 107 | 108 | 'enable_formdata' => false, 109 | 110 | /* 111 | |-------------------------------------------------------------------------- 112 | | Parse Form Request Rules 113 | |-------------------------------------------------------------------------- 114 | | 115 | | If you want form requests to be printed in the field description field, 116 | | and if so, whether they will be in a human readable form. 117 | | 118 | */ 119 | 120 | 'print_rules' => true, // @requires: 'enable_formdata' === true 121 | 'rules_to_human_readable' => true, // @requires: 'parse_rules' === true 122 | 123 | /* 124 | |-------------------------------------------------------------------------- 125 | | Form Data 126 | |-------------------------------------------------------------------------- 127 | | 128 | | The key/values to requests for form data dummy information. 129 | | 130 | */ 131 | 132 | 'formdata' => [ 133 | // 'email' => 'john@example.com', 134 | // 'password' => 'changeme', 135 | ], 136 | 137 | /* 138 | |-------------------------------------------------------------------------- 139 | | Include Middleware 140 | |-------------------------------------------------------------------------- 141 | | 142 | | The routes of the included middleware are included in the export. 143 | | 144 | */ 145 | 146 | 'include_middleware' => ['api'], 147 | 148 | /* 149 | |-------------------------------------------------------------------------- 150 | | Disk Driver 151 | |-------------------------------------------------------------------------- 152 | | 153 | | Specify the configured disk for storing the postman collection file. 154 | | 155 | */ 156 | 157 | 'disk' => 'local', 158 | 159 | /* 160 | |-------------------------------------------------------------------------- 161 | | Authentication 162 | |-------------------------------------------------------------------------- 163 | | 164 | | Specify the authentication to be used for the endpoints. 165 | | 166 | */ 167 | 168 | 'authentication' => [ 169 | 'method' => env('POSTMAN_EXPORT_AUTH_METHOD'), 170 | 'token' => env('POSTMAN_EXPORT_AUTH_TOKEN'), 171 | ], 172 | 173 | /* 174 | |-------------------------------------------------------------------------- 175 | | Protocol Profile Behavior 176 | |-------------------------------------------------------------------------- 177 | | 178 | | Set of configurations used to alter the usual behavior of sending the request. 179 | | These can be defined in a collection at Item or ItemGroup level which will be inherited if applicable. 180 | | 181 | */ 182 | 183 | 'protocol_profile_behavior' => [ 184 | 'disable_body_pruning' => false, // Control request body pruning for following methods: GET, COPY, HEAD, PURGE, UNLOCK 185 | ], 186 | 187 | ]; 188 | -------------------------------------------------------------------------------- /examples/2021_02_04_151948_postman.json: -------------------------------------------------------------------------------- 1 | { 2 | "variable": [{ 3 | "key": "base_url", 4 | "value": "https:\/\/api.example.com\/" 5 | }, { 6 | "key": "token", 7 | "value": "123456789" 8 | }], 9 | "info": { 10 | "name": "2021_02_04_151948_postman", 11 | "schema": "https:\/\/schema.getpostman.com\/json\/collection\/v2.1.0\/collection.json" 12 | }, 13 | "item": [{ 14 | "name": "api", 15 | "request": { 16 | "method": "GET", 17 | "header": [{ 18 | "key": "Content-Type", 19 | "value": "application\/json" 20 | }], 21 | "url": { 22 | "raw": "{{base_url}}\/api", 23 | "host": "{{base_url}}\/api" 24 | } 25 | } 26 | }, { 27 | "name": "api\/posts", 28 | "request": { 29 | "method": "GET", 30 | "header": [{ 31 | "key": "Content-Type", 32 | "value": "application\/json" 33 | }], 34 | "url": { 35 | "raw": "{{base_url}}\/api\/posts", 36 | "host": "{{base_url}}\/api\/posts" 37 | } 38 | } 39 | }, { 40 | "name": "api\/posts", 41 | "request": { 42 | "method": "POST", 43 | "header": [{ 44 | "key": "Content-Type", 45 | "value": "application\/json" 46 | }], 47 | "url": { 48 | "raw": "{{base_url}}\/api\/posts", 49 | "host": "{{base_url}}\/api\/posts" 50 | } 51 | } 52 | }, { 53 | "name": "api\/posts\/{post}", 54 | "request": { 55 | "method": "GET", 56 | "header": [{ 57 | "key": "Content-Type", 58 | "value": "application\/json" 59 | }], 60 | "url": { 61 | "raw": "{{base_url}}\/api\/posts\/{post}", 62 | "host": "{{base_url}}\/api\/posts\/{post}" 63 | } 64 | } 65 | }, { 66 | "name": "api\/posts\/{post}", 67 | "request": { 68 | "method": "PUT", 69 | "header": [{ 70 | "key": "Content-Type", 71 | "value": "application\/json" 72 | }], 73 | "url": { 74 | "raw": "{{base_url}}\/api\/posts\/{post}", 75 | "host": "{{base_url}}\/api\/posts\/{post}" 76 | } 77 | } 78 | }, { 79 | "name": "api\/posts\/{post}", 80 | "request": { 81 | "method": "PATCH", 82 | "header": [{ 83 | "key": "Content-Type", 84 | "value": "application\/json" 85 | }], 86 | "url": { 87 | "raw": "{{base_url}}\/api\/posts\/{post}", 88 | "host": "{{base_url}}\/api\/posts\/{post}" 89 | } 90 | } 91 | }, { 92 | "name": "api\/posts\/{post}", 93 | "request": { 94 | "method": "DELETE", 95 | "header": [{ 96 | "key": "Content-Type", 97 | "value": "application\/json" 98 | }], 99 | "url": { 100 | "raw": "{{base_url}}\/api\/posts\/{post}", 101 | "host": "{{base_url}}\/api\/posts\/{post}" 102 | } 103 | } 104 | }, { 105 | "name": "api\/user", 106 | "request": { 107 | "method": "POST", 108 | "header": [{ 109 | "key": "Content-Type", 110 | "value": "application\/json" 111 | }, { 112 | "key": "Authorization", 113 | "value": "Bearer {{token}}" 114 | }], 115 | "url": { 116 | "raw": "{{base_url}}\/api\/user", 117 | "host": "{{base_url}}\/api\/user" 118 | } 119 | } 120 | }] 121 | } 122 | -------------------------------------------------------------------------------- /examples/2021_02_04_155327_postman.json: -------------------------------------------------------------------------------- 1 | { 2 | "variable": [{ 3 | "key": "base_url", 4 | "value": "https:\/\/api.example.com\/" 5 | }, { 6 | "key": "token", 7 | "value": "123456789" 8 | }], 9 | "info": { 10 | "name": "2021_02_04_155327_postman", 11 | "schema": "https:\/\/schema.getpostman.com\/json\/collection\/v2.1.0\/collection.json" 12 | }, 13 | "item": [{ 14 | "name": "home", 15 | "item": [{ 16 | "name": "api", 17 | "request": { 18 | "method": "GET", 19 | "header": [{ 20 | "key": "Content-Type", 21 | "value": "application\/json" 22 | }], 23 | "url": { 24 | "raw": "{{base_url}}\/api", 25 | "host": "{{base_url}}\/api" 26 | } 27 | } 28 | }] 29 | }, { 30 | "name": "posts", 31 | "item": [{ 32 | "name": "api\/posts", 33 | "request": { 34 | "method": "GET", 35 | "header": [{ 36 | "key": "Content-Type", 37 | "value": "application\/json" 38 | }], 39 | "url": { 40 | "raw": "{{base_url}}\/api\/posts", 41 | "host": "{{base_url}}\/api\/posts" 42 | } 43 | } 44 | }, { 45 | "name": "api\/posts", 46 | "request": { 47 | "method": "POST", 48 | "header": [{ 49 | "key": "Content-Type", 50 | "value": "application\/json" 51 | }], 52 | "url": { 53 | "raw": "{{base_url}}\/api\/posts", 54 | "host": "{{base_url}}\/api\/posts" 55 | } 56 | } 57 | }, { 58 | "name": "api\/posts\/{post}", 59 | "request": { 60 | "method": "GET", 61 | "header": [{ 62 | "key": "Content-Type", 63 | "value": "application\/json" 64 | }], 65 | "url": { 66 | "raw": "{{base_url}}\/api\/posts\/{post}", 67 | "host": "{{base_url}}\/api\/posts\/{post}" 68 | } 69 | } 70 | }, { 71 | "name": "api\/posts\/{post}", 72 | "request": { 73 | "method": "PUT", 74 | "header": [{ 75 | "key": "Content-Type", 76 | "value": "application\/json" 77 | }], 78 | "url": { 79 | "raw": "{{base_url}}\/api\/posts\/{post}", 80 | "host": "{{base_url}}\/api\/posts\/{post}" 81 | } 82 | } 83 | }, { 84 | "name": "api\/posts\/{post}", 85 | "request": { 86 | "method": "PATCH", 87 | "header": [{ 88 | "key": "Content-Type", 89 | "value": "application\/json" 90 | }], 91 | "url": { 92 | "raw": "{{base_url}}\/api\/posts\/{post}", 93 | "host": "{{base_url}}\/api\/posts\/{post}" 94 | } 95 | } 96 | }, { 97 | "name": "api\/posts\/{post}", 98 | "request": { 99 | "method": "DELETE", 100 | "header": [{ 101 | "key": "Content-Type", 102 | "value": "application\/json" 103 | }], 104 | "url": { 105 | "raw": "{{base_url}}\/api\/posts\/{post}", 106 | "host": "{{base_url}}\/api\/posts\/{post}" 107 | } 108 | } 109 | }] 110 | }, { 111 | "name": "user", 112 | "item": [{ 113 | "name": "api\/user", 114 | "request": { 115 | "method": "POST", 116 | "header": [{ 117 | "key": "Content-Type", 118 | "value": "application\/json" 119 | }, { 120 | "key": "Authorization", 121 | "value": "Bearer {{token}}" 122 | }], 123 | "url": { 124 | "raw": "{{base_url}}\/api\/user", 125 | "host": "{{base_url}}\/api\/user" 126 | } 127 | } 128 | }] 129 | }] 130 | } 131 | -------------------------------------------------------------------------------- /examples/api.php: -------------------------------------------------------------------------------- 1 | name('home'); 8 | 9 | Route::apiResource('posts', 'PostController'); 10 | 11 | Route::middleware('auth:api')->group(function () { 12 | Route::post('/user', UserController::class)->name('user'); 13 | }); 14 | -------------------------------------------------------------------------------- /header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreaselia/laravel-api-to-postman/24d1fa6764bcc0cfd84bfd745ec7c6f0d7de5776/header.png -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | src/ 16 | 17 | 18 | 19 | 20 | ./tests/Unit 21 | 22 | 23 | ./tests/Feature 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /phpunit.xml.bak: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | src/ 16 | 17 | 18 | 19 | 20 | ./tests/Unit 21 | 22 | 23 | ./tests/Feature 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Authentication/AuthenticationMethod.php: -------------------------------------------------------------------------------- 1 | 'Authorization', 18 | 'value' => sprintf('%s %s', $this->prefix(), $this->token ?? '{{token}}'), 19 | ]; 20 | } 21 | 22 | public function getToken(): string 23 | { 24 | return $this->token; 25 | } 26 | 27 | abstract public function prefix(): string; 28 | } 29 | -------------------------------------------------------------------------------- /src/Authentication/Basic.php: -------------------------------------------------------------------------------- 1 | set('api-postman.authentication', [ 29 | 'method' => $this->option('bearer') ? 'bearer' : ($this->option('basic') ? 'basic' : null), 30 | 'token' => $this->option('bearer') ?? $this->option('basic') ?? null, 31 | ]); 32 | 33 | $exporter 34 | ->to($filename) 35 | ->setAuthentication(value(function () { 36 | if (filled($this->option('bearer'))) { 37 | return new \AndreasElia\PostmanGenerator\Authentication\Bearer($this->option('bearer')); 38 | } 39 | 40 | if (filled($this->option('basic'))) { 41 | return new \AndreasElia\PostmanGenerator\Authentication\Basic($this->option('basic')); 42 | } 43 | 44 | return null; 45 | })) 46 | ->export(); 47 | 48 | Storage::disk(config('api-postman.disk')) 49 | ->put('postman/'.$filename, $exporter->getOutput()); 50 | 51 | $this->info('Postman Collection Exported: '.storage_path('app/postman/'.$filename)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Concerns/HasAuthentication.php: -------------------------------------------------------------------------------- 1 | config['authentication']; 15 | 16 | if ($config['method']) { 17 | $className = Str::of('AndreasElia\\PostmanGenerator\\Authentication\\') 18 | ->append(ucfirst($config['method'])) 19 | ->toString(); 20 | 21 | $this->authentication = new $className($config['token']); 22 | } 23 | 24 | return $this; 25 | } 26 | 27 | public function setAuthentication(?AuthenticationMethod $authentication): self 28 | { 29 | if (isset($authentication)) { 30 | $this->authentication = $authentication; 31 | } 32 | 33 | return $this; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Exporter.php: -------------------------------------------------------------------------------- 1 | config = $config['api-postman']; 22 | } 23 | 24 | public function to(string $filename): self 25 | { 26 | $this->filename = $filename; 27 | 28 | return $this; 29 | } 30 | 31 | public function getOutput() 32 | { 33 | return json_encode($this->output); 34 | } 35 | 36 | public function export(): void 37 | { 38 | $this->resolveAuth(); 39 | 40 | $this->output = $this->generateStructure(); 41 | } 42 | 43 | protected function generateStructure(): array 44 | { 45 | $this->output = [ 46 | 'variable' => [ 47 | [ 48 | 'key' => 'base_url', 49 | 'value' => $this->config['base_url'], 50 | ], 51 | ], 52 | 'info' => [ 53 | 'name' => $this->filename, 54 | 'schema' => 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json', 55 | ], 56 | 'item' => [], 57 | 'event' => [], 58 | ]; 59 | 60 | $preRequestPath = $this->config['prerequest_script']; 61 | $testPath = $this->config['test_script']; 62 | 63 | if ($preRequestPath || $testPath) { 64 | $scripts = [ 65 | 'prerequest' => $preRequestPath, 66 | 'test' => $testPath, 67 | ]; 68 | 69 | foreach ($scripts as $type => $path) { 70 | if (file_exists($path)) { 71 | $this->output['event'][] = [ 72 | 'listen' => $type, 73 | 'script' => [ 74 | 'type' => 'text/javascript', 75 | 'exec' => file_get_contents($path), 76 | ], 77 | ]; 78 | } 79 | } 80 | } 81 | 82 | if ($this->authentication) { 83 | $this->output['variable'][] = [ 84 | 'key' => 'token', 85 | 'value' => $this->authentication->getToken(), 86 | ]; 87 | } 88 | 89 | return app(RouteProcessor::class)->process($this->output); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/PostmanGeneratorServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 18 | $this->publishes([ 19 | __DIR__.'/../config/api-postman.php' => config_path('api-postman.php'), 20 | ], 'postman-config'); 21 | } 22 | 23 | $this->commands(ExportPostmanCommand::class); 24 | } 25 | 26 | /** 27 | * Register any application services. 28 | * 29 | * @return void 30 | */ 31 | public function register() 32 | { 33 | $this->mergeConfigFrom( 34 | __DIR__.'/../config/api-postman.php', 'api-postman' 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Processors/DocBlockProcessor.php: -------------------------------------------------------------------------------- 1 | getDocComment(); 27 | $tokens = new TokenIterator($lexer->tokenize($comment)); 28 | $phpDocNode = $parser->parse($tokens); 29 | 30 | foreach ($phpDocNode->children as $child) { 31 | if ($child instanceof PhpDocTextNode) { 32 | $description .= ' '.$child->text; 33 | } 34 | } 35 | 36 | return Str::squish($description); 37 | } catch (Throwable $e) { 38 | return ''; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Processors/FormDataProcessor.php: -------------------------------------------------------------------------------- 1 | getParameters()) 17 | ->first(function ($value) { 18 | $value = $value->getType(); 19 | 20 | return $value && is_subclass_of($value->getName(), FormRequest::class); 21 | }); 22 | 23 | if ($rulesParameter) { 24 | /** @var FormRequest $class */ 25 | $class = new ($rulesParameter->getType()->getName()); 26 | 27 | $classRules = method_exists($class, 'rules') ? $class->rules() : []; 28 | 29 | foreach ($classRules as $fieldName => $rule) { 30 | if (is_string($rule)) { 31 | $rule = preg_split('/\s*\|\s*/', $rule); 32 | } 33 | 34 | $printRules = config('api-postman.print_rules'); 35 | 36 | $rules->push([ 37 | 'name' => $fieldName, 38 | 'description' => $printRules ? $rule : '', 39 | ]); 40 | 41 | if (is_array($rule) && in_array('confirmed', $rule)) { 42 | $rules->push([ 43 | 'name' => $fieldName.'_confirmation', 44 | 'description' => $printRules ? $rule : '', 45 | ]); 46 | } 47 | } 48 | } 49 | 50 | return $rules; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Processors/RouteProcessor.php: -------------------------------------------------------------------------------- 1 | config = $config['api-postman']; 33 | 34 | $this->router = $router; 35 | 36 | $this->resolveAuth(); 37 | } 38 | 39 | public function process(array $output): array 40 | { 41 | $this->output = $output; 42 | 43 | $routes = collect($this->router->getRoutes()); 44 | 45 | /** @var Route $route */ 46 | foreach ($routes as $route) { 47 | $this->processRoute($route); 48 | } 49 | 50 | return $this->output; 51 | } 52 | 53 | /** 54 | * @throws \ReflectionException 55 | */ 56 | protected function processRoute(Route $route) 57 | { 58 | try { 59 | $methods = array_filter($route->methods(), fn ($value) => $value !== 'HEAD'); 60 | $middlewares = $route->gatherMiddleware(); 61 | 62 | foreach ($methods as $method) { 63 | $includedMiddleware = false; 64 | 65 | foreach ($middlewares as $middleware) { 66 | if (in_array($middleware, $this->config['include_middleware'])) { 67 | $includedMiddleware = true; 68 | } 69 | } 70 | 71 | if (empty($middlewares) || ! $includedMiddleware) { 72 | continue; 73 | } 74 | 75 | $reflectionMethod = $this->getReflectionMethod($route->getAction()); 76 | 77 | if (! $reflectionMethod) { 78 | continue; 79 | } 80 | 81 | $routeHeaders = $this->config['headers']; 82 | 83 | if ($this->authentication && in_array($this->config['auth_middleware'], $middlewares)) { 84 | $routeHeaders[] = $this->authentication->toArray(); 85 | } 86 | 87 | $uri = Str::of($route->uri())->replaceMatches('/{([[:alnum:]_]+)}/', ':$1'); 88 | 89 | if ($this->config['include_doc_comments']) { 90 | $description = (new DocBlockProcessor)($reflectionMethod); 91 | } 92 | 93 | $data = [ 94 | 'name' => $route->uri(), 95 | 'request' => array_merge( 96 | $this->processRequest( 97 | $method, 98 | $uri, 99 | $this->config['enable_formdata'] ? (new FormDataProcessor)->process($reflectionMethod) : collect() 100 | ), 101 | ['description' => $description ?? ''] 102 | ), 103 | 'response' => [], 104 | 105 | 'protocolProfileBehavior' => [ 106 | 'disableBodyPruning' => $this->config['protocol_profile_behavior']['disable_body_pruning'] ?? false, 107 | ], 108 | ]; 109 | 110 | if ($this->config['structured']) { 111 | $routeNameSegments = ( 112 | $route->getName() 113 | ? Str::of($route->getName())->explode('.') 114 | : Str::of($route->uri())->after('api/')->explode('/') 115 | )->filter(fn ($value) => ! is_null($value) && $value !== ''); 116 | 117 | if (! $this->config['crud_folders']) { 118 | if (in_array($routeNameSegments->last(), ['index', 'store', 'show', 'update', 'destroy'])) { 119 | $routeNameSegments->forget($routeNameSegments->count() - 1); 120 | } 121 | } 122 | 123 | $this->buildTree($this->output, $routeNameSegments->all(), $data); 124 | } else { 125 | $this->output['item'][] = $data; 126 | } 127 | } 128 | } catch (\Exception $e) { 129 | Log::warning('Failed to process route: '.$route->uri()); 130 | } 131 | } 132 | 133 | protected function processRequest(string $method, Stringable $uri, Collection $rules): array 134 | { 135 | return collect([ 136 | 'method' => strtoupper($method), 137 | 'header' => collect($this->config['headers']) 138 | ->push($this->authentication?->toArray()) 139 | ->filter() 140 | ->values() 141 | ->all(), 142 | 'url' => [ 143 | 'raw' => '{{base_url}}/'.$uri, 144 | 'host' => ['{{base_url}}'], 145 | 'path' => $uri->explode('/')->filter()->all(), 146 | 'variable' => $uri 147 | ->matchAll('/(?<={)[[:alnum:]]+(?=})/m') 148 | ->transform(function ($variable) { 149 | return ['key' => $variable, 'value' => '']; 150 | }) 151 | ->all(), 152 | ], 153 | ]) 154 | ->when($rules, function (Collection $collection, Collection $rules) use ($method) { 155 | if ($rules->isEmpty()) { 156 | return $collection; 157 | } 158 | 159 | $rules->transform(fn ($rule) => [ 160 | 'key' => $rule['name'], 161 | 'value' => $this->config['formdata'][$rule['name']] ?? null, 162 | 'description' => $this->config['print_rules'] ? $this->parseRulesIntoHumanReadable($rule['name'], $rule['description']) : null, 163 | ]); 164 | 165 | if ($method === 'GET') { 166 | return $collection->mergeRecursive([ 167 | 'url' => [ 168 | 'query' => $rules->map(fn ($value) => array_merge($value, ['disabled' => false])), 169 | ], 170 | ]); 171 | } 172 | 173 | return $collection->put('body', [ 174 | 'mode' => 'urlencoded', 175 | 'urlencoded' => $rules->map(fn ($value) => array_merge($value, ['type' => 'text'])), 176 | ]); 177 | }) 178 | ->all(); 179 | } 180 | 181 | protected function processResponse(string $method, array $action): array 182 | { 183 | return [ 184 | 'code' => 200, 185 | 'body' => [ 186 | 'mode' => 'raw', 187 | 'raw' => '', 188 | ], 189 | ]; 190 | } 191 | 192 | /** 193 | * @throws \ReflectionException 194 | */ 195 | private function getReflectionMethod(array $routeAction): ?object 196 | { 197 | if ($this->containsSerializedClosure($routeAction)) { 198 | $routeAction['uses'] = unserialize($routeAction['uses'])->getClosure(); 199 | } 200 | 201 | if ($routeAction['uses'] instanceof Closure) { 202 | return new ReflectionFunction($routeAction['uses']); 203 | } 204 | 205 | $routeData = explode('@', $routeAction['uses']); 206 | $reflection = new ReflectionClass($routeData[0]); 207 | 208 | if (! $reflection->hasMethod($routeData[1])) { 209 | return null; 210 | } 211 | 212 | return $reflection->getMethod($routeData[1]); 213 | } 214 | 215 | private function containsSerializedClosure(array $action): bool 216 | { 217 | return is_string($action['uses']) && Str::startsWith($action['uses'], [ 218 | 'C:32:"Opis\\Closure\\SerializableClosure', 219 | 'O:47:"Laravel\SerializableClosure\\SerializableClosure', 220 | 'O:55:"Laravel\\SerializableClosure\\UnsignedSerializableClosure', 221 | ]); 222 | } 223 | 224 | protected function buildTree(array &$routes, array $segments, array $request): void 225 | { 226 | $parent = &$routes; 227 | $destination = end($segments); 228 | 229 | foreach ($segments as $segment) { 230 | $matched = false; 231 | 232 | foreach ($parent['item'] as &$item) { 233 | if ($item['name'] === $segment) { 234 | $parent = &$item; 235 | 236 | if ($segment === $destination) { 237 | $parent['item'][] = $request; 238 | } 239 | 240 | $matched = true; 241 | 242 | break; 243 | } 244 | } 245 | 246 | unset($item); 247 | 248 | if (! $matched) { 249 | $item = [ 250 | 'name' => $segment, 251 | 'item' => $segment === $destination ? [$request] : [], 252 | ]; 253 | 254 | $parent['item'][] = &$item; 255 | $parent = &$item; 256 | } 257 | 258 | unset($item); 259 | } 260 | } 261 | 262 | protected function parseRulesIntoHumanReadable($attribute, $rules): string 263 | { 264 | // ... bail if user has asked for non interpreted strings: 265 | if (! $this->config['rules_to_human_readable']) { 266 | foreach ($rules as $i => $rule) { 267 | // because we don't support custom rule classes, we remove them from the rules 268 | if (is_subclass_of($rule, Rule::class)) { 269 | unset($rules[$i]); 270 | } 271 | } 272 | 273 | return is_array($rules) ? implode(', ', $rules) : $this->safelyStringifyClassBasedRule($rules); 274 | } 275 | 276 | /* 277 | * An object based rule is presumably a Laravel default class based rule or one that implements the Illuminate 278 | * Rule interface. Lets try to safely access the string representation... 279 | */ 280 | if (is_object($rules)) { 281 | $rules = [$this->safelyStringifyClassBasedRule($rules)]; 282 | } 283 | 284 | /* 285 | * Handle string based rules (e.g. required|string|max:30) 286 | */ 287 | if (is_array($rules)) { 288 | foreach ($rules as $i => $rule) { 289 | if (is_object($rule)) { 290 | unset($rules[$i]); 291 | } 292 | } 293 | 294 | $validator = Validator::make([], [$attribute => implode('|', $rules)]); 295 | 296 | foreach ($rules as $rule) { 297 | [$rule, $parameters] = ValidationRuleParser::parse($rule); 298 | 299 | $validator->addFailure($attribute, $rule, $parameters); 300 | } 301 | 302 | $messages = $validator->getMessageBag()->toArray()[$attribute]; 303 | 304 | if (is_array($messages)) { 305 | $messages = $this->handleEdgeCases($messages); 306 | } 307 | 308 | return implode(', ', is_array($messages) ? $messages : $messages->toArray()); 309 | } 310 | 311 | // ...safely return a safe value if we encounter neither a string or object based rule set: 312 | return ''; 313 | } 314 | 315 | protected function handleEdgeCases(array $messages): array 316 | { 317 | foreach ($messages as $key => $message) { 318 | if ($message === 'validation.nullable') { 319 | $messages[$key] = '(Nullable)'; 320 | 321 | continue; 322 | } 323 | 324 | if ($message === 'validation.sometimes') { 325 | $messages[$key] = '(Optional)'; 326 | } 327 | } 328 | 329 | return $messages; 330 | } 331 | 332 | /** 333 | * In this case we have received what is most likely a Rule Object but are not certain. 334 | */ 335 | protected function safelyStringifyClassBasedRule($probableRule): string 336 | { 337 | if (! is_object($probableRule) || is_subclass_of($probableRule, Rule::class) || ! method_exists($probableRule, '__toString')) { 338 | return ''; 339 | } 340 | 341 | return (string) $probableRule; 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /tests/Feature/ExportPostmanTest.php: -------------------------------------------------------------------------------- 1 | set('api-postman.filename', 'test.json'); 19 | 20 | Storage::disk()->deleteDirectory('postman'); 21 | } 22 | 23 | /** 24 | * @dataProvider providerFormDataEnabled 25 | */ 26 | public function test_standard_export_works(bool $formDataEnabled) 27 | { 28 | config()->set('api-postman.enable_formdata', $formDataEnabled); 29 | 30 | $this->artisan('export:postman')->assertExitCode(0); 31 | 32 | $collection = json_decode(Storage::get('postman/'.config('api-postman.filename')), true); 33 | 34 | $routes = $this->app['router']->getRoutes(); 35 | 36 | $collectionItems = $collection['item']; 37 | 38 | $totalCollectionItems = $this->countCollectionItems($collection['item']); 39 | 40 | $this->assertEquals(count($routes), $totalCollectionItems); 41 | 42 | foreach ($routes as $route) { 43 | $methods = $route->methods(); 44 | 45 | $collectionRoutes = Arr::where($collectionItems, function ($item) use ($route) { 46 | return $item['name'] == $route->uri(); 47 | }); 48 | 49 | $collectionRoute = Arr::first($collectionRoutes); 50 | 51 | if (! in_array($collectionRoute['request']['method'], $methods)) { 52 | $methods = collect($collectionRoutes)->pluck('request.method')->toArray(); 53 | } 54 | 55 | $this->assertNotNull($collectionRoute); 56 | $this->assertTrue(in_array($collectionRoute['request']['method'], $methods)); 57 | } 58 | } 59 | 60 | /** 61 | * @dataProvider providerFormDataEnabled 62 | */ 63 | public function test_bearer_export_works(bool $formDataEnabled) 64 | { 65 | config()->set('api-postman.enable_formdata', $formDataEnabled); 66 | 67 | $this->artisan('export:postman --bearer=1234567890')->assertExitCode(0); 68 | 69 | $collection = json_decode(Storage::get('postman/'.config('api-postman.filename')), true); 70 | 71 | $routes = $this->app['router']->getRoutes(); 72 | 73 | $collectionVariables = $collection['variable']; 74 | 75 | foreach ($collectionVariables as $variable) { 76 | if ($variable['key'] != 'token') { 77 | continue; 78 | } 79 | 80 | $this->assertEquals($variable['value'], '1234567890'); 81 | } 82 | 83 | $this->assertCount(2, $collectionVariables); 84 | 85 | $totalCollectionItems = $this->countCollectionItems($collection['item']); 86 | 87 | $this->assertEquals(count($routes), $totalCollectionItems); 88 | 89 | foreach ($routes as $route) { 90 | $methods = $route->methods(); 91 | 92 | $collectionRoutes = Arr::where($collection['item'], function ($item) use ($route) { 93 | return $item['name'] == $route->uri(); 94 | }); 95 | 96 | $collectionRoute = Arr::first($collectionRoutes); 97 | 98 | if (! in_array($collectionRoute['request']['method'], $methods)) { 99 | $methods = collect($collectionRoutes)->pluck('request.method')->toArray(); 100 | } 101 | 102 | $this->assertNotNull($collectionRoute); 103 | $this->assertTrue(in_array($collectionRoute['request']['method'], $methods)); 104 | } 105 | } 106 | 107 | /** 108 | * @dataProvider providerFormDataEnabled 109 | */ 110 | public function test_basic_export_works(bool $formDataEnabled) 111 | { 112 | config()->set('api-postman.enable_formdata', $formDataEnabled); 113 | 114 | $this->artisan('export:postman --basic=username:password1234')->assertExitCode(0); 115 | 116 | $collection = json_decode(Storage::get('postman/'.config('api-postman.filename')), true); 117 | 118 | $routes = $this->app['router']->getRoutes(); 119 | 120 | $collectionVariables = $collection['variable']; 121 | 122 | foreach ($collectionVariables as $variable) { 123 | if ($variable['key'] != 'token') { 124 | continue; 125 | } 126 | 127 | $this->assertEquals($variable['value'], 'username:password1234'); 128 | } 129 | 130 | $this->assertCount(2, $collectionVariables); 131 | 132 | $totalCollectionItems = $this->countCollectionItems($collection['item']); 133 | 134 | $this->assertEquals(count($routes), $totalCollectionItems); 135 | 136 | foreach ($routes as $route) { 137 | $methods = $route->methods(); 138 | 139 | $collectionRoutes = Arr::where($collection['item'], function ($item) use ($route) { 140 | return $item['name'] == $route->uri(); 141 | }); 142 | 143 | $collectionRoute = Arr::first($collectionRoutes); 144 | 145 | if (! in_array($collectionRoute['request']['method'], $methods)) { 146 | $methods = collect($collectionRoutes)->pluck('request.method')->toArray(); 147 | } 148 | 149 | $this->assertNotNull($collectionRoute); 150 | $this->assertTrue(in_array($collectionRoute['request']['method'], $methods)); 151 | } 152 | } 153 | 154 | /** 155 | * @dataProvider providerFormDataEnabled 156 | */ 157 | public function test_structured_export_works(bool $formDataEnabled) 158 | { 159 | config([ 160 | 'api-postman.structured' => true, 161 | 'api-postman.enable_formdata' => $formDataEnabled, 162 | ]); 163 | 164 | $this->artisan('export:postman')->assertExitCode(0); 165 | 166 | $collection = json_decode(Storage::get('postman/'.config('api-postman.filename')), true); 167 | 168 | $routes = $this->app['router']->getRoutes(); 169 | 170 | $totalCollectionItems = $this->countCollectionItems($collection['item']); 171 | 172 | $this->assertEquals(count($routes), $totalCollectionItems); 173 | } 174 | 175 | public function test_rules_printing_export_works() 176 | { 177 | config([ 178 | 'api-postman.enable_formdata' => true, 179 | 'api-postman.print_rules' => true, 180 | 'api-postman.rules_to_human_readable' => false, 181 | ]); 182 | 183 | $this->artisan('export:postman')->assertExitCode(0); 184 | 185 | $collection = collect(json_decode(Storage::get('postman/'.config('api-postman.filename')), true)['item']); 186 | 187 | $targetRequest = $collection 188 | ->where('name', 'example/storeWithFormRequest') 189 | ->first(); 190 | 191 | $fields = collect($targetRequest['request']['body']['urlencoded']); 192 | $this->assertCount(1, $fields->where('key', 'field_1')->where('description', 'required')); 193 | $this->assertCount(1, $fields->where('key', 'field_2')->where('description', 'required, integer')); 194 | $this->assertCount(1, $fields->where('key', 'field_5')->where('description', 'required, integer, max:30, min:1')); 195 | $this->assertCount(1, $fields->where('key', 'field_6')->where('description', 'in:"1","2","3"')); 196 | } 197 | 198 | public function test_rules_printing_get_export_works() 199 | { 200 | config([ 201 | 'api-postman.enable_formdata' => true, 202 | 'api-postman.print_rules' => true, 203 | 'api-postman.rules_to_human_readable' => false, 204 | ]); 205 | 206 | $this->artisan('export:postman')->assertExitCode(0); 207 | 208 | $this->assertTrue(true); 209 | 210 | $collection = collect(json_decode(Storage::get('postman/'.config('api-postman.filename')), true)['item']); 211 | 212 | $targetRequest = $collection 213 | ->where('name', 'example/getWithFormRequest') 214 | ->first(); 215 | 216 | $this->assertEqualsCanonicalizing([ 217 | 'raw' => '{{base_url}}/example/getWithFormRequest', 218 | 'host' => [ 219 | '{{base_url}}', 220 | ], 221 | 'path' => [ 222 | 'example', 223 | 'getWithFormRequest', 224 | ], 225 | 'variable' => [], 226 | ], array_slice($targetRequest['request']['url'], 0, 4)); 227 | 228 | $fields = collect($targetRequest['request']['url']['query']); 229 | $this->assertCount(1, $fields->where('key', 'field_1')->where('description', 'required')); 230 | $this->assertCount(1, $fields->where('key', 'field_2')->where('description', 'required, integer')); 231 | $this->assertCount(1, $fields->where('key', 'field_5')->where('description', 'required, integer, max:30, min:1')); 232 | $this->assertCount(1, $fields->where('key', 'field_6')->where('description', 'in:"1","2","3"')); 233 | 234 | // Check for the required structure of the get request query 235 | foreach ($fields as $field) { 236 | $this->assertEqualsCanonicalizing([ 237 | 'key' => $field['key'], 238 | 'value' => null, 239 | 'disabled' => false, 240 | 'description' => $field['description'], 241 | ], $field); 242 | } 243 | } 244 | 245 | public function test_rules_printing_export_to_human_readable_works() 246 | { 247 | config([ 248 | 'api-postman.enable_formdata' => true, 249 | 'api-postman.print_rules' => true, 250 | 'api-postman.rules_to_human_readable' => true, 251 | ]); 252 | 253 | $this->artisan('export:postman')->assertExitCode(0); 254 | 255 | $collection = collect(json_decode(Storage::get('postman/'.config('api-postman.filename')), true)['item']); 256 | 257 | $targetRequest = $collection 258 | ->where('name', 'example/storeWithFormRequest') 259 | ->first(); 260 | 261 | $fields = collect($targetRequest['request']['body']['urlencoded']); 262 | $this->assertCount(1, $fields->where('key', 'field_1')->where('description', 'The field 1 field is required.')); 263 | $this->assertCount(1, $fields->where('key', 'field_2')->where('description', 'The field 2 field is required., The field 2 field must be an integer.')); 264 | $this->assertCount(1, $fields->where('key', 'field_3')->where('description', '(Optional), The field 3 field must be an integer.')); 265 | $this->assertCount(1, $fields->where('key', 'field_4')->where('description', '(Nullable), The field 4 field must be an integer.')); 266 | // the below fails locally, but passes on GitHub actions? 267 | $this->assertCount(1, $fields->where('key', 'field_5')->where('description', 'The field 5 field is required., The field 5 field must be an integer., The field 5 field must not be greater than 30., The field 5 field must be at least 1.')); 268 | 269 | /** This looks bad, but this is the default message in lang/en/validation.php, you can update to:. 270 | * 271 | * "'in' => 'The selected :attribute is invalid. Allowable values: :values'," 272 | **/ 273 | $this->assertCount(1, $fields->where('key', 'field_6')->where('description', 'The selected field 6 is invalid.')); 274 | $this->assertCount(1, $fields->where('key', 'field_7')->where('description', 'The field 7 field is required.')); 275 | $this->assertCount(1, $fields->where('key', 'field_8')->where('description', 'The field 8 field must be uppercase.')); 276 | $this->assertCount(1, $fields->where('key', 'field_9')->where('description', 'The field 9 field is required., The field 9 field must be a string.')); 277 | } 278 | 279 | public function test_event_export_works() 280 | { 281 | $eventScriptPath = 'tests/Fixtures/ExampleEvent.js'; 282 | 283 | config([ 284 | 'api-postman.prerequest_script' => $eventScriptPath, 285 | 'api-postman.test_script' => $eventScriptPath, 286 | ]); 287 | 288 | $this->artisan('export:postman')->assertExitCode(0); 289 | 290 | $collection = collect(json_decode(Storage::get('postman/'.config('api-postman.filename')), true)['event']); 291 | 292 | $events = $collection 293 | ->whereIn('listen', ['prerequest', 'test']) 294 | ->all(); 295 | 296 | $this->assertCount(2, $events); 297 | 298 | $content = file_get_contents($eventScriptPath); 299 | 300 | foreach ($events as $event) { 301 | $this->assertEquals($event['script']['exec'], $content); 302 | } 303 | } 304 | 305 | public function test_php_doc_comment_export() 306 | { 307 | config([ 308 | 'api-postman.include_doc_comments' => true, 309 | ]); 310 | 311 | $this->artisan('export:postman')->assertExitCode(0); 312 | 313 | $collection = collect(json_decode(Storage::get('postman/'.config('api-postman.filename')), true)['item']); 314 | 315 | $targetRequest = $collection 316 | ->where('name', 'example/phpDocRoute') 317 | ->first(); 318 | 319 | $this->assertEquals($targetRequest['request']['description'], 'This is the php doc route. Which is also multi-line. and has a blank line.'); 320 | } 321 | 322 | public function test_uri_is_correct() 323 | { 324 | $this->artisan('export:postman')->assertExitCode(0); 325 | 326 | $collection = collect(json_decode(Storage::get('postman/'.config('api-postman.filename')), true)['item']); 327 | 328 | $targetRequest = $collection 329 | ->where('name', 'example/phpDocRoute') 330 | ->first(); 331 | 332 | $this->assertEquals($targetRequest['name'], 'example/phpDocRoute'); 333 | $this->assertEquals($targetRequest['request']['url']['raw'], '{{base_url}}/example/phpDocRoute'); 334 | } 335 | 336 | public function test_api_resource_routes_set_parameters_correctly_with_hyphens() 337 | { 338 | $this->artisan('export:postman')->assertExitCode(0); 339 | 340 | $collection = collect(json_decode(Storage::get('postman/'.config('api-postman.filename')), true)['item']); 341 | 342 | $targetRequest = $collection 343 | ->where('name', 'example/users/{user}/audit-logs/{audit_log}') 344 | ->where('request.method', 'PATCH') 345 | ->first(); 346 | 347 | $this->assertEquals($targetRequest['name'], 'example/users/{user}/audit-logs/{audit_log}'); 348 | $this->assertEquals($targetRequest['request']['url']['raw'], '{{base_url}}/example/users/:user/audit-logs/:audit_log'); 349 | } 350 | 351 | public function test_api_resource_routes_set_parameters_correctly_with_underscores() 352 | { 353 | $this->artisan('export:postman')->assertExitCode(0); 354 | 355 | $collection = collect(json_decode(Storage::get('postman/'.config('api-postman.filename')), true)['item']); 356 | 357 | $targetRequest = $collection 358 | ->where('name', 'example/users/{user}/other_logs/{other_log}') 359 | ->where('request.method', 'PATCH') 360 | ->first(); 361 | 362 | $this->assertEquals($targetRequest['name'], 'example/users/{user}/other_logs/{other_log}'); 363 | $this->assertEquals($targetRequest['request']['url']['raw'], '{{base_url}}/example/users/:user/other_logs/:other_log'); 364 | } 365 | 366 | public function test_api_resource_routes_set_parameters_correctly_with_camel_case() 367 | { 368 | $this->artisan('export:postman')->assertExitCode(0); 369 | 370 | $collection = collect(json_decode(Storage::get('postman/'.config('api-postman.filename')), true)['item']); 371 | 372 | $targetRequest = $collection 373 | ->where('name', 'example/users/{user}/someLogs/{someLog}') 374 | ->where('request.method', 'PATCH') 375 | ->first(); 376 | 377 | $this->assertEquals($targetRequest['name'], 'example/users/{user}/someLogs/{someLog}'); 378 | $this->assertEquals($targetRequest['request']['url']['raw'], '{{base_url}}/example/users/:user/someLogs/:someLog'); 379 | } 380 | 381 | public static function providerFormDataEnabled(): array 382 | { 383 | return [ 384 | [ 385 | false, 386 | ], 387 | [ 388 | true, 389 | ], 390 | ]; 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /tests/Feature/ExportPostmanWithCacheTest.php: -------------------------------------------------------------------------------- 1 | defineCacheRoutes(<<<'PHP' 18 | group(function () { 20 | Route::get('serialized-route', function () { 21 | return 'Serialized Route'; 22 | }); 23 | }); 24 | PHP); 25 | 26 | config()->set('api-postman.filename', 'test.json'); 27 | 28 | Storage::disk()->deleteDirectory('postman'); 29 | } 30 | 31 | public function test_cached_export_works() 32 | { 33 | $this->markTestSkipped('Vendor routes are included in the cached routes, so this test fails'); 34 | 35 | $this->get('serialized-route') 36 | ->assertOk() 37 | ->assertSee('Serialized Route'); 38 | 39 | $this->artisan('export:postman')->assertExitCode(0); 40 | 41 | $collection = json_decode(Storage::get('postman/'.config('api-postman.filename')), true); 42 | 43 | $routes = $this->app['router']->getRoutes()->getRoutesByName(); 44 | 45 | // Filter out workbench routes from orchestra/workbench 46 | $routes = array_filter($routes, function ($route) { 47 | return strpos($route->uri(), 'workbench') === false; 48 | }); 49 | 50 | $collectionItems = $collection['item']; 51 | 52 | $this->assertCount(count($routes), $collectionItems); 53 | 54 | foreach ($routes as $route) { 55 | $collectionRoute = Arr::first($collectionItems, function ($item) use ($route) { 56 | return $item['name'] == $route->uri(); 57 | }); 58 | $this->assertNotNull($collectionRoute); 59 | $this->assertTrue(in_array($collectionRoute['request']['method'], $route->methods())); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/Fixtures/AuditLogController.php: -------------------------------------------------------------------------------- 1 | retrieveRoutes($item); 19 | } 20 | 21 | return $sum; 22 | } 23 | 24 | return 1; 25 | } 26 | 27 | private function countCollectionItems(array $collectionItems) 28 | { 29 | $sum = 0; 30 | 31 | foreach ($collectionItems as $item) { 32 | $sum += $this->retrieveRoutes($item); 33 | } 34 | 35 | return $sum; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Fixtures/ExampleController.php: -------------------------------------------------------------------------------- 1 | getRequestData(); 32 | } 33 | 34 | public function storeWithFormRequest(ExampleFormRequest $request): string 35 | { 36 | return 'storeWithFormRequest'; 37 | } 38 | 39 | public function getWithFormRequest(ExampleFormRequest $request): string 40 | { 41 | return 'getWithFormRequest'; 42 | } 43 | 44 | /** 45 | * This is the php doc route. 46 | * Which is also multi-line. 47 | * 48 | * and has a blank line. 49 | * 50 | * @param string $non-existing param 51 | */ 52 | public function phpDocRoute(): string 53 | { 54 | return 'phpDocRoute'; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Fixtures/ExampleEvent.js: -------------------------------------------------------------------------------- 1 | console.log("example event"); 2 | -------------------------------------------------------------------------------- /tests/Fixtures/ExampleFormRequest.php: -------------------------------------------------------------------------------- 1 | 'required', 14 | 'field_2' => 'required|integer', 15 | 'field_3' => 'sometimes|integer', 16 | 'field_4' => 'nullable|integer', 17 | 'field_5' => 'required|integer|max:30|min:1', 18 | 'field_6' => new In([1, 2, 3]), 19 | 'field_7' => ['required', new In([1, 2, 3])], 20 | 'field_8' => new UppercaseRule, 21 | 'field_9' => ['required', 'string', new UppercaseRule], 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Fixtures/ExampleModel.php: -------------------------------------------------------------------------------- 1 | request = $request; 14 | } 15 | 16 | public function getRequestData(): array 17 | { 18 | return $this->request->all(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Fixtures/UppercaseRule.php: -------------------------------------------------------------------------------- 1 | middleware('api')->prefix('example')->name('example.')->group(function ($router) { 18 | $router->get('index', [ExampleController::class, 'index'])->name('index'); 19 | $router->get('show', [ExampleController::class, 'show'])->name('show'); 20 | $router->post('store', [ExampleController::class, 'store'])->name('store'); 21 | $router->delete('delete', [ExampleController::class, 'delete'])->name('delete'); 22 | $router->get('showWithReflectionMethod', [ExampleController::class, 'showWithReflectionMethod'])->name('show-with-reflection-method'); 23 | $router->post('storeWithFormRequest', [ExampleController::class, 'storeWithFormRequest'])->name('store-with-form-request'); 24 | $router->get('getWithFormRequest', [ExampleController::class, 'getWithFormRequest'])->name('get-with-form-request'); 25 | $router->get('phpDocRoute', [ExampleController::class, 'phpDocRoute'])->name('php-doc-route'); 26 | $router->apiResource('users.audit-logs', AuditLogController::class); 27 | $router->apiResource('users.other_logs', AuditLogController::class); 28 | $router->apiResource('users.someLogs', AuditLogController::class); 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreaselia/laravel-api-to-postman/24d1fa6764bcc0cfd84bfd745ec7c6f0d7de5776/tests/Unit/.gitkeep --------------------------------------------------------------------------------