├── .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 | [](https://thewebtier.com)
2 |
3 | [](https://packagist.org/packages/usamamuneerchaudhary/laraclient)
4 | [](https://scrutinizer-ci.com/g/usamamuneerchaudhary/laraclient/?branch=main)
5 | [](https://www.codefactor.io/repository/github/usamamuneerchaudhary/laraclient)
6 | [](https://scrutinizer-ci.com/g/usamamuneerchaudhary/laraclient/build-status/main)
7 | [](https://scrutinizer-ci.com/code-intelligence)
8 | [](https://packagist.org/packages/usamamuneerchaudhary/laraclient)
9 | [](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 |
--------------------------------------------------------------------------------