├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── composer.lock ├── config └── lara_client.php ├── migrations └── 2023_02_24_000000_create_laraclient_logs_table.php ├── phpunit.xml ├── resources └── views │ └── logs │ └── index.blade.php ├── routes └── web.php ├── src ├── .editorconfig ├── Contracts │ └── LaraClientInterface.php ├── Exceptions │ └── LaraClientApiClientException.php ├── Http │ └── Controllers │ │ ├── Controller.php │ │ └── LogsController.php ├── LaraClient.php ├── LaraClientServiceProvider.php ├── Models │ └── LaraClientLog.php └── Response.php └── tests ├── LaraClientGetTest.php └── TestCase.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Usama Muneer 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 | [![image](https://www.linkpicture.com/q/laraclient.jpg)](https://thewebtier.com) 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/usamamuneerchaudhary/laraclient?style=flat-square)](https://packagist.org/packages/usamamuneerchaudhary/laraclient) 4 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/usamamuneerchaudhary/laraclient/badges/quality-score.png?b=main)](https://scrutinizer-ci.com/g/usamamuneerchaudhary/laraclient/?branch=main) 5 | [![CodeFactor](https://www.codefactor.io/repository/github/usamamuneerchaudhary/laraclient/badge)](https://www.codefactor.io/repository/github/usamamuneerchaudhary/laraclient) 6 | [![Build Status](https://scrutinizer-ci.com/g/usamamuneerchaudhary/laraclient/badges/build.png?b=main)](https://scrutinizer-ci.com/g/usamamuneerchaudhary/laraclient/build-status/main) 7 | [![Code Intelligence Status](https://scrutinizer-ci.com/g/usamamuneerchaudhary/laraclient/badges/code-intelligence.svg?b=main)](https://scrutinizer-ci.com/code-intelligence) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/usamamuneerchaudhary/laraclient?style=flat-square)](https://packagist.org/packages/usamamuneerchaudhary/laraclient) 9 | [![Licence](https://img.shields.io/packagist/l/usamamuneerchaudhary/laraclient?style=flat-square)](https://github.com/usamamuneerchaudhary/laraclient/blob/HEAD/LICENSE.md) 10 | ## Introduction 11 | Lara Client simplifies the process of working with APIs in Laravel, making it easy to handle authentication, rate 12 | limiting, and error handling. 13 | It allows to set up several API connections in a central configuration file, specifying the credentials for each 14 | connection. 15 | The package also includes a cache layer to speed up requests, and a logging system to track API requests and responses for debugging purposes. 16 | With Lara Client, developers can quickly integrate multiple APIs into their Laravel applications, reducing development 17 | time and effort, and making it easier to manage API integrations over time. 18 | 19 | Here's a quick example of what you can do in your models to enable tagging: 20 | 21 | - *This package supports Laravel 10* 22 | - *Minimum PHP v8.1 supported* 23 | 24 | ```php 25 | namespace App\Http\Controllers; 26 | 27 | use Usamamuneerchaudhary\LaraClient\LaraClient; 28 | 29 | class ApiController extends Controller 30 | { 31 | $client = new LaraClient('weatherapi'); 32 | $response = $client->get('current.json', ['q' => 'london']); 33 | $currentWeather = $response->getData(); 34 | 35 | $client2 = new LaraClient('geodb'); 36 | $georesponse = $client->get('countries'); 37 | $countries = $response->getData(); 38 | 39 | .... 40 | .... 41 | 42 | } 43 | ``` 44 | 45 | ## Installation 46 | You can install the package via composer: 47 | 48 | `composer require usamamuneerchaudhary/laraclient` 49 | ## Run Migrations 50 | 51 | Once the package is installed, you can run migrations, 52 | 53 | `php artisan migrate` 54 | 55 | 56 | ## Publish Config File 57 | ``` 58 | php artisan vendor:publish --provider="Usamamuneerchaudhary\LaraClient\LaraClientServiceProvider" --tag="config" 59 | ``` 60 | This will create a `lara_client.php` file, where you can define multiple third party API connections. 61 | 62 | ## Service Provider 63 | 64 | Don't forget to add the ServiceProvider in `app.php`: 65 | 66 | ``` 67 | \Usamamuneerchaudhary\LaraClient\LaraClientServiceProvider::class, 68 | ``` 69 | ## Logging & Publish Views 70 | 71 | We're using a logging table to store requests and responses, you can access this by the following route: 72 | ``` 73 | http://yourwebsite.com/laraclient/logs 74 | 75 | //to access logs for any specific endpoint 76 | http://yourwebsite.com/laraclient/logs?endpoint=https://weatherapi-com.p.rapidapi.com/current.json 77 | ``` 78 | ## Tutorial 79 | [How to handle multiple API connections in Laravel](https://thewebtier.com/how-to-handle-multiple-api-connections-in-laravel) 80 | 81 | ## Tests 82 | `composer test` 83 | 84 | ## Security 85 | If you discover any security related issues, please email hello@usamamuneer.me instead of using the issue tracker. 86 | 87 | ## Credits 88 | 89 | - [Laravel](https://laravel.com) 90 | - [Usama Muneer](https://usamamuneer.me) 91 | - [All Contributors](https://github.com/usamamuneerchaudhary/laraclient/graphs/contributors) 92 | 93 | ## License 94 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 95 | 96 | 97 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "usamamuneerchaudhary/laraclient", 3 | "description": "Package that simplifies the process of working with multiple APIs in Laravel.", 4 | "keywords": [ 5 | "usamamuneerchaudhary", 6 | "laravel-client", 7 | "api", 8 | "guzzlehttp", 9 | "http", 10 | "httpclient" 11 | ], 12 | "license": "MIT", 13 | "type": "library", 14 | "authors": [ 15 | { 16 | "name": "Usama Muneer", 17 | "email": "hello@usamamuneer.me", 18 | "role": "Developer" 19 | } 20 | ], 21 | "autoload": { 22 | "psr-4": { 23 | "Usamamuneerchaudhary\\LaraClient\\": "src" 24 | } 25 | }, 26 | "require": { 27 | "php": "^8.1", 28 | "illuminate/database": ">=v10.1.4", 29 | "illuminate/support": ">=v10.1.4", 30 | "guzzlehttp/guzzle": "^7.5" 31 | }, 32 | "require-dev": { 33 | "phpunit/phpunit": "^10.0", 34 | "orchestra/testbench": "^v8.0.4" 35 | }, 36 | "autoload-dev": { 37 | "classmap": [ 38 | "tests/TestCase.php" 39 | ] 40 | }, 41 | "scripts": { 42 | "test": "vendor/bin/phpunit" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /config/lara_client.php: -------------------------------------------------------------------------------- 1 | env('API_CLIENT', 'api'), 5 | 6 | 'connections' => [ 7 | 'api' => [ 8 | 'base_uri' => env('API_BASE_URI', 'https://api.example.com/'), 9 | 'api_key' => env('API_API_KEY', ''), 10 | 'default_headers' => [ 11 | 'Accept' => 'application/json', 12 | 'Content-Type' => 'application/json', 13 | ], 14 | 'api_secret' => env('API_API_SECRET', ''), 15 | 'timeout' => env('API_TIMEOUT', 30), 16 | 'rate_limit' => [ 17 | 'limit' => env('API_RATE_LIMIT', 60), 18 | 'interval' => env('API_RATE_LIMIT_INTERVAL', 60), 19 | ], 20 | ], 21 | 'otherapi' => [ 22 | // OtherApi configuration 23 | ], 24 | ], 25 | ]; 26 | -------------------------------------------------------------------------------- /migrations/2023_02_24_000000_create_laraclient_logs_table.php: -------------------------------------------------------------------------------- 1 | id(); 15 | $table->string('endpoint'); 16 | $table->string('method'); 17 | $table->text('request_payload')->nullable(); 18 | $table->integer('response_status'); 19 | $table->text('response_body')->nullable(); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | */ 27 | public function down(): void 28 | { 29 | Schema::dropIfExists('laraclient_logs'); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | ./tests 13 | 14 | 15 | 16 | 17 | ./src 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /resources/views/logs/index.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Lara Client Logs 7 | 8 | 9 | 10 | 37 | 38 | 39 | 40 | @if(count($logs)>=1) 41 | @foreach($logs as $log) 42 |

Endpoint: {{$log->endpoint}}({{$log->method}})

43 | Created: {{\Carbon\Carbon::createFromTimeStamp(strtotime($log->created_at))->diffForHumans()}} 44 |
45 |
46 |

Request

47 |

48 |                 {!! $log->request_payload !!}
49 |             
50 |
51 |
52 |

Response

53 |

54 |                     {!! $log->response_body !!}
55 |             
56 |
57 |
58 |
59 | @endforeach 60 | {{$logs->links('pagination::bootstrap-4')}} 61 | @else 62 |

No logs found

63 | @endif 64 | 65 | 66 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | name('logs.index'); 7 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /src/Contracts/LaraClientInterface.php: -------------------------------------------------------------------------------- 1 | statusCode = $statusCode; 22 | $this->response = $response; 23 | } 24 | 25 | /** 26 | * @return mixed|null 27 | */ 28 | public function getStatusCode(): mixed 29 | { 30 | return $this->statusCode; 31 | } 32 | 33 | /** 34 | * @return mixed|null 35 | */ 36 | public function getResponse(): mixed 37 | { 38 | return $this->response; 39 | } 40 | 41 | /** 42 | * @return void 43 | */ 44 | public function report(): void 45 | { 46 | Log::debug('HTTP Error with Lara Client API'); 47 | } 48 | 49 | /** 50 | * @param $request 51 | * @return \Illuminate\Http\JsonResponse 52 | */ 53 | public function render($request): \Illuminate\Http\JsonResponse 54 | { 55 | return response()->json(["error" => true, "message" => $this->getStatusCode()]); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | get('endpoint')) { 18 | $logs = LaraClientLog::where('endpoint', request()->get('endpoint'))->orderBy('created_at', 19 | 'desc')->paginate(10); 20 | } else { 21 | $logs = LaraClientLog::orderBy('created_at', 'desc')->paginate(10); 22 | } 23 | return view('laraclient::logs.index', compact('logs')); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/LaraClient.php: -------------------------------------------------------------------------------- 1 | config = Config::get('lara_client.connections.'.($connection ?: Config::get('lara_client.default'))); 26 | $this->httpClient = new Client([ 27 | 'base_uri' => $this->config['base_uri'], 28 | 'headers' => $this->config['default_headers'], 29 | 'timeout' => $this->config['timeout'], 30 | ]); 31 | } 32 | 33 | /** 34 | * @param $uri 35 | * @param $queryParams 36 | * @return Response 37 | * @throws GuzzleException 38 | * @throws LaraClientApiClientException 39 | */ 40 | public function get($uri, $queryParams = []): Response 41 | { 42 | $fullUrl = $this->getFullUrl($uri); 43 | return $this->request('GET', $fullUrl, ['query' => $queryParams]); 44 | } 45 | 46 | /** 47 | * @param $uri 48 | * @param $data 49 | * @return Response 50 | * @throws GuzzleException 51 | * @throws LaraClientApiClientException 52 | */ 53 | public function post($uri, $data = []): Response 54 | { 55 | $fullUrl = $this->getFullUrl($uri); 56 | return $this->request('POST', $fullUrl, ['json' => $data]); 57 | } 58 | 59 | /** 60 | * @param $uri 61 | * @param $data 62 | * @return Response 63 | * @throws GuzzleException 64 | * @throws LaraClientApiClientException 65 | */ 66 | public function put($uri, $data = []): Response 67 | { 68 | $fullUrl = $this->getFullUrl($uri); 69 | return $this->request('PUT', $fullUrl, ['json' => $data]); 70 | } 71 | 72 | /** 73 | * @param $uri 74 | * @param $data 75 | * @return Response 76 | * @throws GuzzleException 77 | * @throws LaraClientApiClientException 78 | */ 79 | public function patch($uri, $data = []): Response 80 | { 81 | $fullUrl = $this->getFullUrl($uri); 82 | return $this->request('PATCH', $fullUrl, ['json' => $data]); 83 | } 84 | 85 | /** 86 | * @param $uri 87 | * @param $data 88 | * @return Response 89 | * @throws GuzzleException 90 | * @throws LaraClientApiClientException 91 | */ 92 | public function delete($uri, $data = []): Response 93 | { 94 | $fullUrl = $this->getFullUrl($uri); 95 | return $this->request('DELETE', $fullUrl, ['json' => $data]); 96 | } 97 | 98 | /** 99 | * @throws LaraClientApiClientException 100 | * @throws GuzzleException 101 | */ 102 | protected function request($method, $uri, $options): Response 103 | { 104 | $options['headers'] = $this->getHeaders(); 105 | 106 | if (Cache::has('api_rate_limit')) { 107 | sleep($this->config['rate_limit']['interval']); 108 | } 109 | 110 | try { 111 | $response = $this->httpClient->request($method, $uri, $options); 112 | $this->logRequest($method, $uri, $options, $response); 113 | $this->handleRateLimit($response->getHeader('X-RateLimit-Reset')); 114 | } catch (RequestException $e) { 115 | $response = $e->getResponse(); 116 | 117 | if ($response->getStatusCode() === 429) { 118 | $this->handleRateLimit($response->getHeader('X-RateLimit-Reset')); 119 | return $this->request($method, $uri, $options); 120 | } 121 | throw new LaraClientApiClientException($response->getStatusCode(), $response->getReasonPhrase()); 122 | } 123 | return new Response($response->getStatusCode(), $response->getBody()); 124 | } 125 | 126 | /** 127 | * @param $additionalHeaders 128 | * @return array 129 | */ 130 | protected function getHeaders($additionalHeaders = []): array 131 | { 132 | // Merge the default headers with any additional headers passed in 133 | $headers = array_merge($this->config['default_headers'], $additionalHeaders); 134 | 135 | // Add the Authorization header if an API key is set 136 | if (!empty($this->config['api_key'])) { 137 | $headers['Authorization'] = 'Bearer '.$this->config['api_key']; 138 | } 139 | 140 | return $headers; 141 | } 142 | 143 | /** 144 | * @param $resetHeader 145 | * @return void 146 | */ 147 | protected function handleRateLimit($resetHeader): void 148 | { 149 | if (!empty($resetHeader)) { 150 | $resetTimestamp = (int) $resetHeader[0]; 151 | $currentTimestamp = time(); 152 | 153 | if ($resetTimestamp > $currentTimestamp) { 154 | $waitTime = $resetTimestamp - $currentTimestamp; 155 | Cache::put('api_rate_limit', true, $waitTime); 156 | } 157 | } 158 | } 159 | 160 | 161 | /** 162 | * @param string $method 163 | * @param string $uri 164 | * @param array $options 165 | * @param $response 166 | * @return void 167 | */ 168 | protected function logRequest(string $method, string $uri, array $options, $response): void 169 | { 170 | $status = $response instanceof ResponseInterface ? $response->getStatusCode() : null; 171 | $responseBody = $response instanceof ResponseInterface ? (string) $response->getBody() : null; 172 | 173 | LaraClientLog::create([ 174 | 'endpoint' => $uri, 175 | 'method' => $method, 176 | 'request_payload' => json_encode($options), 177 | 'response_status' => $status, 178 | 'response_body' => $responseBody, 179 | 'created_at' => now() 180 | ]); 181 | } 182 | 183 | /** 184 | * @param $uri 185 | * @return string 186 | */ 187 | protected function getFullUrl($uri): string 188 | { 189 | $fullUrl = $uri; 190 | if (!preg_match('/^https?:\/\//', $uri)) { 191 | $fullUrl = $this->config['base_uri'].$uri; 192 | } 193 | return $fullUrl; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/LaraClientServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom(__DIR__.'/../config/lara_client.php', 'lara_client'); 17 | } 18 | 19 | /** 20 | * @return void 21 | */ 22 | public function boot(): void 23 | { 24 | if ($this->app->runningInConsole()) { 25 | // Publish config file 26 | $this->publishes([ 27 | __DIR__.'/../config/lara_client.php' => config_path('lara_client.php'), 28 | ], 'config'); 29 | // Publish views 30 | $this->publishes([ 31 | __DIR__.'/../resources/views' => resource_path('views/vendor/laraclient'), 32 | ], 'views'); 33 | } 34 | $this->loadMigrationsFrom(__DIR__.'/../migrations'); 35 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'laraclient'); 36 | $this->registerRoutes(); 37 | } 38 | 39 | /** 40 | * @return void 41 | */ 42 | protected function registerRoutes(): void 43 | { 44 | Route::group($this->routeConfiguration(), function () { 45 | $this->loadRoutesFrom(__DIR__.'/../routes/web.php'); 46 | }); 47 | } 48 | 49 | /** 50 | * @return string[] 51 | */ 52 | protected function routeConfiguration(): array 53 | { 54 | return [ 55 | 'prefix' => 'laraclient', 56 | // 'middleware'=>'' 57 | ]; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Models/LaraClientLog.php: -------------------------------------------------------------------------------- 1 | statusCode = $statusCode; 17 | $this->data = $data; 18 | } 19 | 20 | /** 21 | * @return mixed 22 | */ 23 | public function getStatusCode(): mixed 24 | { 25 | return $this->statusCode; 26 | } 27 | 28 | /** 29 | * @return mixed 30 | */ 31 | public function getData(): mixed 32 | { 33 | return json_decode($this->data); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/LaraClientGetTest.php: -------------------------------------------------------------------------------- 1 | get('countries'); 12 | $data = $response->getData(); 13 | 14 | $this->assertIsObject($data); 15 | $arr = (array) $data; 16 | $this->assertArrayHasKey('metadata', $arr); 17 | $this->assertArrayHasKey('links', $arr); 18 | $this->assertArrayHasKey('data', $arr); 19 | } 20 | 21 | /** @test */ 22 | public function it_throws_exception_for_failed_requests() 23 | { 24 | $client = new LaraClient('weatherapi'); 25 | $this->expectException(\Usamamuneerchaudhary\LaraClient\Exceptions\LaraClientApiClientException::class); 26 | $client->get('/wrong.json', ['q' => 'london']); 27 | } 28 | 29 | /** @test */ 30 | public function get_can_have_extra_params() 31 | { 32 | $client = new LaraClient('weatherapi'); 33 | $response = $client->get('current.json', ['q' => 'london']); 34 | $data = $response->getData(); 35 | $this->assertIsObject($data); 36 | $arr = (array) $data; 37 | $this->assertArrayHasKey('location', $arr); 38 | $this->assertArrayHasKey('current', $arr); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | artisan('migrate', [ 25 | '--database' => 'testbench', 26 | '--realpath' => realpath(__DIR__.'/../migrations') 27 | ]); 28 | } 29 | 30 | 31 | /** 32 | * @param $app 33 | * @return void 34 | */ 35 | protected function getEnvironmentSetUp($app) 36 | { 37 | $app['config']->set('database.default', 'testbench'); 38 | $app['config']->set('database.connections.testbench', [ 39 | 'driver' => 'sqlite', 40 | 'database' => ':memory:', 41 | 'prefix' => '' 42 | ]); 43 | $app['config']->set('lara_client.default', 'geodb'); 44 | $app['config']->set('lara_client.connections.geodb.base_uri', 45 | 'https://wft-geo-db.p.rapidapi.com/v1/geo/'); 46 | $app['config']->set('lara_client.connections.geodb.default_headers', [ 47 | 'Accept' => 'application/json', 48 | 'Content-Type' => 'application/json', 49 | 'X-RapidAPI-Key' => '23c6b0817bmsh5720c5bcb04bb86p151c03jsn8f683b20fbe2', 50 | 'X-RapidAPI-Host' => 'wft-geo-db.p.rapidapi.com' 51 | ]); 52 | $app['config']->set('lara_client.connections.geodb.timeout', 30); 53 | 54 | $app['config']->set('lara_client.connections.weatherapi.base_uri', 55 | 'https://weatherapi-com.p.rapidapi.com/'); 56 | $app['config']->set('lara_client.connections.weatherapi.default_headers', [ 57 | 'Accept' => 'application/json', 58 | 'Content-Type' => 'application/json', 59 | 'X-RapidAPI-Key' => '23c6b0817bmsh5720c5bcb04bb86p151c03jsn8f683b20fbe2' 60 | ]); 61 | $app['config']->set('lara_client.connections.weatherapi.timeout', 30); 62 | } 63 | 64 | } 65 | --------------------------------------------------------------------------------