yuriitatur / api-client
Boilerplate for building api clients
Requires
- php: >=8.2
- guzzlehttp/guzzle: ^7.4
- psr/log: ^3.0
- yuriitatur/exceptions: ^1.0
Requires (Dev)
- dg/bypass-finals: ^1.9
- kamermans/guzzle-oauth2-subscriber: ^1.0
- kevinrob/guzzle-cache-middleware: ^6.0
- kint-php/kint: ^4.1
- laravel/framework: ^12.20
- phpunit/phpunit: ^12.0
- symfony/lock: ^7.3
- symfony/rate-limiter: ^7.3
- symfony/yaml: ^6.1
- yuriitatur/laravel-logger-boost: ^1.0
Suggests
- glopgar/monolog-timer-processor: Logs request duration
- kamermans/guzzle-oauth2-subscriber: Adds middleware for Oauth2 authentication
- kevinrob/guzzle-cache-middleware: Adds middleware for caching
- symfony/lock: while using rate limiters, adds ability to eliminate race conditions
- symfony/rate-limiter: adds ability to use rate limiters
- symfony/yaml: Enables decoding Yaml responses
- yuriitatur/laravel-logger-boost: With it, you can pass log keys, that you are using in your system to add tracability
This package is auto-updated.
Last update: 2025-08-11 19:21:11 UTC
README
Basic Api Client
A boilerplate to quickly start using your own api client.
Installation
composer require yuriitatur/api-client
Usage
Configuration
use YuriiTatur\ApiClient\Services\StackBuilder; use GuzzleHttp\Client; use YuriiTatur\ApiClient\BasicApiClient; use YuriiTatur\ApiClient\ApiClientBuilder; use YuriiTatur\ApiClient\Services\Retry\MaxCountRetryStrategy; $api = (new ApiClientBuilder) ->withLogger($logger) ->withBaseUri('https://3rd-part-api.com') ->withGuzzleOptions([ # default guzzle options 'verify' => $isProduction, 'auth' => ['my-user', 'my-password'] ]) ->withCustomStack(function (StackBuilder $stack) { # create stack via builder or return your own return $stack->withHandler(new CurlHandler()) ->withNoExceptionConverter() ->withRetryStrategy(new MaxCountRetryStrategy(2)) ->build(); }) ->build();
- Making calls as usual
/**@var $api \YuriiTatur\ApiClient\BasicApiClient */ $api->request('GET', 'https://google.com', [ 'query' => [ 'foo' => 'bar' ], ]);
Rate limiting Middleware
To use rate limiting middleware, you have to install symfony/rate-limiter
library.
For more advanced usage, to atomically track requests, you may want to install symfony/lock
package.
First, you need to create a middlewares. You can create as many middlewares as you have different limit cases.
For example, let's create 2 middlewares to handle /v1 and /v2 routes.
composer require symfony/rate-limiter symfony/lock
use YuriiTatur\ApiClient\Middleware\RateLimitMiddleware;
use Symfony\Component\RateLimiter\RateLimiterFactory;
use Symfony\Component\RateLimiter\Storage\InMemoryStorage;
use Symfony\Component\Lock\Store\InMemoryStore;
use Symfony\Component\Lock\LockFactory;
# for more at https://symfony.com/doc/current/rate_limiter.html
$factoryV1 = new RateLimiterFactory([
'id' => 'rate-limits-for-v1-routes',
'policy' => 'sliding_window',
'limit' => 100, # requests
'interval' => '1 minute',
], new InMemoryStorage(), new LockFactory(new InMemoryStore()));
$factoryV2 = new RateLimiterFactory([
'id' => 'rate-limits-for-v2-routes',
'policy' => 'sliding_window',
'limit' => 10000, # requests
'interval' => '1 hour',
], new InMemoryStorage(), new LockFactory(new InMemoryStore()));
$rateLimitV1 = new RateLimitMiddleware(
'v1-routes',
$factory->create('v1-routes-limiter'),
$logger,
10 # this is a max waiting time to make a request reservation
);
$rateLimitV2 = new RateLimitMiddleware(
'v2-routes',
$factory->create('v2-routes-limiter'),
$logger,
);
$stack->push($rateLimitV1, 'v1-limit');
$stack->push($rateLimitV2, 'v2-limit');
After registering those middlewares in our stack, we are ready to use it, simply pass
rate_limiter
option in your request.
$api->get('https://api.domain.com/v1/users', [
'rate_limiter' => 'v1-routes',
]);
$api->get('https://api.domain.com/v2/users', [
'rate_limiter' => 'v2-routes',
]);
You may think why we need to specify limiters explicitly? if it's v1 => v1-routes. There are thousands of different combinations of request options. Some apis rate limit by api keys, some by route prefixes, others by called entities. You'll have to write your own logic. Simply create a new middleware and register it in stack like so: Note that this is very simplified code.
class MatchRateLimiterMiddleware {
public function __invoke(callable $next) {
return function (RequestInterface $request, array $options) use ($next) {
if (strpos($request->getRequestTarget(), '/v1/') !== false) {
$options['rate_limiter'] = 'v1-routes';
}
if (strpos($request->getRequestTarget(), '/v2/') !== false) {
$options['rate_limiter'] = 'v2-routes';
}
return $next($request, $options);
};
}
}
$stack->before('v1-limit', new MatchRateLimiterMiddleware, 'rate-limit-matcher');
Laravel usage
This package contains a laravel service provider, being registered via composer. First that you want to do is to publish configs.
php artisan vendor:publish --tag=api-clients-config --force
This will create a file in your config directory called api-clients.php
that contains api clients configuration.
It looks something like this
return [
'clients' => [
'my-api-client' => [ # name of the client
'options' => [ # guzzle options
'base_uri' => 'https://api.domain.com',
'verify' => !env('APP_DEBUG'),
],
'response_transformers' => [], # extra response transformers
'exception_converter' => GenericExceptionConverter::class,
'tap' => null, # a simple callable, that modifies your stack
'retry' => [ # retry settings
'retry_strategy' => MaxCountRetryStrategy::class,
'delay_strategy' => ConstantDelayStrategy::class,
],
],
],
'rate_limiters' => [
'default' => [ # name of the rate limiter
'storage' => CacheStorage::class, # storate of made requests
'lock_store' => RedisStore::class, # atomic lock storage
'policy' => 'sliding_window', # token_bucket, fixed_window
'limit' => 1, # number of requests
'interval' => '5 minutes', # time
'rate' => [ # token bucket config
'interval' => '2 seconds', # spawn time
'amount' => 1, # token count
],
'max_wait_time' => 5, # seconds to obtain a request reservation
]
],
];
All these clients are being registered, and available from DI:
app('api-client.my-api-client');
Testing
You can test usage of this api client the same way you test a regular guzzle client.
Either use MockHandler
in your stack or spin-up a mock server. More on that here
To run library tests, run
composer run test
License
This code is under MIT license, read more in the LICENSE file.