├── LICENSE.md ├── README.md ├── composer.json ├── config └── mock-api.php ├── database ├── factories │ └── ModelFactory.php └── migrations │ ├── create_mock_api_url_history_table.php.stub │ └── create_mock_api_url_table.php.stub ├── resources └── views │ └── .gitkeep └── src ├── Facades └── MockApi.php ├── MockApi.php ├── MockApiServiceProvider.php └── Models ├── MockApiUrl.php └── MockApiUrlHistory.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) lichtner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel MockApi 2 | 3 | *Laravel MockAPI is a powerful yet lightweight library designed for mocking external APIs and web services.* 4 | 5 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/lichtner/laravel-mock-api.svg?style=flat-square)](https://packagist.org/packages/lichtner/laravel-mock-api) 6 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/lichtner/laravel-mock-api/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/lichtner/laravel-mock-api/actions?query=workflow%3Arun-tests+branch%3Amain) 7 | [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/lichtner/laravel-mock-api/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/lichtner/laravel-mock-api/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/lichtner/laravel-mock-api.svg?style=flat-square)](https://packagist.org/packages/lichtner/laravel-mock-api) 9 | 10 | ## Why? 11 | 12 | Are you using external APIs and web services during development that are also undergoing development? Are they unstable, slow, occasionally returning incorrect results, or unexpectedly unavailable? Are they causing you headaches? Me too! That was the reason why I created MockApi. 13 | 14 | After installation and setup, MockApi will save all requests from your external web services in the background, and when they are unavailable, return them just like real APIs. 15 | 16 | ## Installation 17 | 18 | ```bash 19 | composer require lichtner/laravel-mock-api 20 | ``` 21 | 22 | Now you can publish config file and run the migrations with: 23 | 24 | ```bash 25 | php artisan mock-api:install 26 | ``` 27 | 28 | ## Setup 29 | 30 | ### Mocking GET request 31 | 32 | Make a simple class that wrap all your `Http::get()` requests e.g.: 33 | 34 | `app/HttpMock.php` 35 | 36 | ```php 37 | 5, 177 | 'title' => 'title 1', 178 | 'body' => 'body 1', 179 | ]); 180 | ``` 181 | 182 | response is: 183 | 184 | ```json 185 | { 186 | "userId": 5, 187 | "title": "title 1", 188 | "body": "body 1" 189 | } 190 | ``` 191 | 192 | Especially for POST request your real API probably add `id` field. To simulate this behaviour you can update field `data` for specific row: 193 | 194 | ```mysql 195 | UPDATE mock_api_url_history SET data='{"id": 1234}' WHERE id = 777; 196 | ``` 197 | 198 | Then same requests response will be: 199 | 200 | ```json 201 | { 202 | "userId": 5, 203 | "title": "title 1", 204 | "body": "body 1", 205 | "id": 1234 206 | } 207 | ``` 208 | 209 | You can add anything in mutation responses (e.g. uuid, etc.). These fields will be merged recursively with your json POST data. 210 | 211 | ### Mock two requests with same url and method 212 | 213 | In table `mock_api_url` is set unique key for (method, url) so you are not able to mock two request with same method and url which is expected behavior. But for specific situation you want to. Maybe you want to mock two different articles with different titles with resource `POST /articles`. To do this you can create special class for that purpose. 214 | 215 | ```php 216 | class HttpMockArticles 217 | { 218 | public static function post(string $url, array $data): Response 219 | { 220 | MockApi::init("$url/$data[title]", $data, 'POST'); 221 | 222 | $response = Http::post($url, $data); 223 | 224 | MockApi::log("$url/$data[title]", $response, 'POST'); 225 | 226 | return $response; 227 | } 228 | } 229 | ``` 230 | 231 | As you can see you can modify `$url` parameter only for `MockApi::init()` and `MockApi::log()` functions, but not for real request `Http::post()`. So two articles with different titles will be saved. 232 | 233 | ## Config 234 | 235 | For more information about configuration check [config/mock-api.php](https://github.com/lichtner/laravel-mock-api/blob/main/config/mock-api.php) 236 | 237 | ## Testing 238 | 239 | ```bash 240 | composer test 241 | ``` 242 | 243 | ## Changelog 244 | 245 | For changelog check [releases](https://github.com/lichtner/laravel-mock-api/releases). 246 | 247 | ## Credits 248 | 249 | - [Marek Lichtner](https://github.com/lichtner) 250 | 251 | ## License 252 | 253 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 254 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lichtner/laravel-mock-api", 3 | "description": "Easy to use, but the powerful micro library for mocking external API", 4 | "keywords": [ 5 | "lichtner", 6 | "laravel", 7 | "laravel-mock-api", 8 | "mocking", 9 | "api" 10 | ], 11 | "homepage": "https://github.com/lichtner/laravel-mock-api", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Marek Lichtner", 16 | "email": "marek.licht@gmail.com", 17 | "role": "Developer" 18 | } 19 | ], 20 | "require": { 21 | "php": "^8.1", 22 | "spatie/laravel-package-tools": "^1.14.0", 23 | "illuminate/contracts": "^10.0" 24 | }, 25 | "require-dev": { 26 | "guzzlehttp/guzzle": "^7.5", 27 | "laravel/pint": "^1.0", 28 | "nunomaduro/collision": "^7.9", 29 | "nunomaduro/larastan": "^2.0.1", 30 | "orchestra/testbench": "^8.0", 31 | "pestphp/pest": "^2.0", 32 | "pestphp/pest-plugin-arch": "^2.0", 33 | "pestphp/pest-plugin-laravel": "^2.0", 34 | "phpstan/extension-installer": "^1.1", 35 | "phpstan/phpstan-deprecation-rules": "^1.0", 36 | "phpstan/phpstan-phpunit": "^1.0" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "Lichtner\\MockApi\\": "src/", 41 | "Lichtner\\MockApi\\Database\\Factories\\": "database/factories/" 42 | } 43 | }, 44 | "autoload-dev": { 45 | "psr-4": { 46 | "Lichtner\\MockApi\\Tests\\": "tests/" 47 | } 48 | }, 49 | "scripts": { 50 | "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", 51 | "analyse": "php vendor/bin/phpstan analyse", 52 | "test": "php vendor/bin/pest", 53 | "test-coverage": "php vendor/bin/pest --coverage", 54 | "format": "php vendor/bin/pint" 55 | }, 56 | "config": { 57 | "sort-packages": true, 58 | "allow-plugins": { 59 | "pestphp/pest-plugin": true, 60 | "phpstan/extension-installer": true 61 | } 62 | }, 63 | "extra": { 64 | "laravel": { 65 | "providers": [ 66 | "Lichtner\\MockApi\\MockApiServiceProvider" 67 | ], 68 | "aliases": { 69 | "MockApi": "Lichtner\\MockApi\\Facades\\MockApi" 70 | } 71 | } 72 | }, 73 | "minimum-stability": "dev", 74 | "prefer-stable": true 75 | } 76 | -------------------------------------------------------------------------------- /config/mock-api.php: -------------------------------------------------------------------------------- 1 | env('MOCK_API', false), 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Mock API status is less than 20 | |-------------------------------------------------------------------------- 21 | | 22 | | 300 means return mocked request with status code less than 300 (200-299) 23 | */ 24 | 25 | 'status' => env('MOCK_API_STATUS', 300), 26 | 27 | /* 28 | |-------------------------------------------------------------------------- 29 | | Mock API environment 30 | |-------------------------------------------------------------------------- 31 | | 32 | | Use Mock API only on this environment 33 | */ 34 | 35 | 'env' => env('MOCK_API_ENV', 'local'), 36 | ]; 37 | -------------------------------------------------------------------------------- /database/factories/ModelFactory.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->unsignedBigInteger('mock_api_url_id'); 14 | $table->unsignedInteger('status'); 15 | $table->string('content_type'); 16 | $table->mediumText('data')->default(''); 17 | $table->timestamp('created_at')->useCurrent(); 18 | $table->index('created_at'); 19 | $table->foreign('mock_api_url_id') 20 | ->references('id') 21 | ->on('mock_api_url') 22 | ->onUpdate('cascade') 23 | ->onDelete('cascade'); 24 | }); 25 | } 26 | 27 | public function down() 28 | { 29 | Schema::dropIfExists('mock_api_url_history'); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /database/migrations/create_mock_api_url_table.php.stub: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->boolean('mock')->default(true); 14 | $table->dateTime('mock_before')->nullable(); 15 | $table->unsignedInteger('mock_status')->nullable(); 16 | $table->unsignedInteger('last_status'); 17 | $table->string('method', 10); 18 | $table->string('url', 500); 19 | $table->timestamps(); 20 | $table->unique(['method', 'url']); 21 | }); 22 | } 23 | 24 | public function down() 25 | { 26 | Schema::dropIfExists('mock_api_url'); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /resources/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lichtner/laravel-mock-api/5cf6e29636168f593091475168e62b1268aa5fb0/resources/views/.gitkeep -------------------------------------------------------------------------------- /src/Facades/MockApi.php: -------------------------------------------------------------------------------- 1 | where('method', strtoupper($method)) 25 | ->firstWhere('mock', 1); 26 | 27 | if (! $mockApiUrl) { 28 | return; 29 | } 30 | 31 | $mockApiUrlHistory = MockApiUrlHistory::where('mock_api_url_id', $mockApiUrl->id); 32 | 33 | if ($mockApiUrl->mock_status) { 34 | $mockApiUrlHistory->where('status', $mockApiUrl->mock_status); 35 | } else { 36 | $mockApiUrlHistory->where('status', '<', config('mock-api.status')); 37 | } 38 | 39 | if ($mockApiUrl->mock_before) { 40 | $mockApiUrlHistory->where('created_at', '<', $mockApiUrl->mock_before); 41 | } 42 | 43 | $history = $mockApiUrlHistory->latest()->first(); 44 | 45 | if (! $history) { 46 | return; 47 | } 48 | 49 | if (is_string($data)) { 50 | $data .= $history->data; 51 | } 52 | if (is_array($data) && str_contains($history->content_type, 'application/json')) { 53 | $merge = json_decode($history->data, true) ?? []; 54 | $data = array_merge_recursive($data, $merge); 55 | } 56 | 57 | Http::fake([ 58 | $mockApiUrl->url => Http::response( 59 | $data, 60 | $history->status, 61 | [ 62 | 'content-type' => $history->content_type, 63 | 'mock-api' => 'true', 64 | ] 65 | ), 66 | ]); 67 | } 68 | 69 | public static function log(string $url, Response $response, string $method = 'GET'): void 70 | { 71 | if (config('app.env') !== config('mock-api.env')) { 72 | return; 73 | } 74 | 75 | if ($response->header('mock-api') === 'true') { 76 | return; 77 | } 78 | 79 | $method = strtoupper($method); 80 | 81 | $mockApi = MockApiUrl::updateOrCreate( 82 | [ 83 | 'method' => $method, 84 | 'url' => $url, 85 | ], [ 86 | 'last_status' => $response->status(), 87 | 'updated_at' => Carbon::now(), 88 | ], 89 | ); 90 | 91 | MockApiUrlHistory::create([ 92 | 'mock_api_url_id' => $mockApi->id, 93 | 'status' => $response->status(), 94 | 'content_type' => $response->header('content-type'), 95 | ...($method === 'GET' ? ['data' => $response->body()] : []), 96 | ]); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/MockApiServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-mock-api') 20 | ->hasConfigFile() 21 | // ->hasViews() 22 | ->hasMigrations([ 23 | 'create_mock_api_url_table', 24 | 'create_mock_api_url_history_table', 25 | ]) 26 | ->hasInstallCommand(function (InstallCommand $command) { 27 | $command 28 | ->publishMigrations() 29 | ->publishConfigFile() 30 | ->askToRunMigrations() 31 | ->askToStarRepoOnGitHub('lichtner/laravel-mock-api'); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Models/MockApiUrl.php: -------------------------------------------------------------------------------- 1 | hasMany(MockApiUrlHistory::class); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Models/MockApiUrlHistory.php: -------------------------------------------------------------------------------- 1 |