silencenjoyer / api-sdk
A lightweight PHP SDK for creating API integrations.
Requires
- php: ^7.4.0|^8.0.0
- ext-json: *
- php-http/discovery: ^1.0
- psr/http-client: ^1.0.0
- psr/http-factory: ^1.0.0
- psr/http-message: ^1.1|^2.0
- psr/log: ~1.1.0
- silencenjoyer/rate-limiter: ^3.0.0
Requires (Dev)
- nyholm/psr7: ^1.8.0
- phpstan/phpstan: *
- phpunit/phpunit: ^9.0
- symfony/http-client: ^5.0
- symfony/var-dumper: ^5.0
Suggests
- guzzlehttp/guzzle: Provides PSR-18 HTTP client
- http-interop/http-factory-guzzle: Provides PSR-17 for Guzzle
- nyholm/psr7: Provides PSR-7 and PSR-17 implementations
- symfony/http-client: Provides PSR-18 HTTP client
README
A lightweight PHP framework for building typed HTTP API integrations.
Requirements
- PHP 7.4 or 8.x
- PSR-18 HTTP client
- PSR-17 HTTP factories (request factory + stream factory)
Installation
composer require silencenjoyer/api-sdk
The package includes php-http/discovery and will auto-discover PSR-18/PSR-17 implementations. Install any compatible package alongside:
# PSR-7 + PSR-17 composer require nyholm/psr7 # PSR-18 client, e.g. one of: composer require symfony/http-client composer require guzzlehttp/guzzle
Overview
The SDK is organized around two layers:
- Layer 1 — end users: consume a ready-made API client (
NbuApi,StripeApi, etc.) - Layer 2 — integration developers: build those clients by extending
AbstractApi
Layer 1: Using an API client
Zero-config instantiation
If php-http/discovery is installed alongside compatible PSR-18/PSR-17 packages, the client discovers them automatically:
$api = NbuApi::build();
Explicit dependencies:
$api = NbuApi::build($httpClient, $requestFactory, $streamFactory);
Advanced builder
For middleware or parser customization, obtain the builder first:
use Silencenjoyer\ApiSdk\Middlewares\LoggerMiddleware; $api = NbuApi::getBuilder() ->withMiddlewares([new LoggerMiddleware($logger)]) ->build();
Authentication
Attach an auth strategy to a built instance. The call is immutable and returns a new instance:
use Silencenjoyer\ApiSdk\Authentication\Bearer; $api = NbuApi::build()->withAuthentication(new Bearer($token));
Implement AuthInterface to create custom strategies (API key header, HMAC signature, etc.).
Executing commands
Two styles are available:
// Immediate execution $response = $api->execute(new GetExchangeRatesCommand(date: '2026-01-01')); // Dispatchable — bind command to the API, send later $command = $api->createCommand(GetExchangeRatesCommand::class, ['date' => '2026-01-01']); // ... pass $command around ... $response = $command->send();
Working with the response
$response->asArray(); // array $response->asObject(); // stdClass $response->getOriginalResponse(); // PSR-7 ResponseInterface
Layer 2: Building an API client
Minimal implementation
use Silencenjoyer\ApiSdk\AbstractApi; use Silencenjoyer\ApiSdk\Commands\CommandInterface; class NbuApi extends AbstractApi { protected function getUrl(): string { return 'https://bank.gov.ua'; } protected function supports(CommandInterface $command): bool { return $command instanceof NbuCommandInterface; } }
Providing default builder configuration
Override getBuilder() to apply defaults that are always needed for this API. The signature must match the parent:
class NbuApi extends AbstractApi { public static function getBuilder( ?ClientInterface $client = null, ?RequestFactoryInterface $requestFactory = null, ?StreamFactoryInterface $streamFactory = null ): ApiBuilderInterface { return parent::getBuilder($client, $requestFactory, $streamFactory) ->withFormats(Format::JSON) ->withMiddlewares([new NbuRetryMiddleware()]); } }
AbstractApi::build() calls static::getBuilder(), so defaults are applied automatically for both NbuApi::build() and NbuApi::getBuilder().
Custom constructor parameters
If your API requires extra constructor arguments (e.g., an environment-specific base URL), add a named factory method that uses withFactory():
class NbuApi extends AbstractApi { private string $baseUrl; public function __construct( string $baseUrl, RequestBuilderInterface $requestBuilder, ResponseParserInterface $responseParser, HandlerInterface $handler, MiddlewareStack $middlewareStack ) { parent::__construct($requestBuilder, $responseParser, $handler, $middlewareStack); $this->baseUrl = $baseUrl; } protected function getUrl(): string { return $this->baseUrl; } public static function forUrl(string $baseUrl): ApiBuilderInterface { return static::getBuilder() ->withFactory(fn($rb, $rp, $h, $ms) => new self($baseUrl, $rb, $rp, $h, $ms)); } } // Usage: $api = NbuApi::forUrl($_ENV['NBU_API_URL'])->build(); $api = NbuApi::forUrl($_ENV['NBU_API_URL'])->withMiddlewares([$logger])->build();
Implementing commands
Extend AbstractCommand and declare the HTTP semantics:
use Silencenjoyer\ApiSdk\Commands\AbstractCommand; use Silencenjoyer\ApiSdk\Constants\HttpMethod; class GetExchangeRatesCommand extends AbstractCommand implements NbuCommandInterface { public function __construct(private string $date) {} protected function isPublic(): bool { return true; } protected function getMethod(): string { return HttpMethod::GET; } protected function getPath(): string { return '/NBUStatService/v1/statdirectory/exchange'; } protected function getQueryParams(): array { return ['json' => '', 'date' => $this->date]; } }
Override getBodyParams(), getHeaders(), getRequestContentType(), or getAnswerContentType() as needed.
For private commands, isPublic() must return false. The authentication strategy attached via withAuthentication() will sign the request automatically.
Per-command middleware
Attach middleware to a specific command class only:
$api = NbuApi::getBuilder() ->withCommandMiddleware(GetExchangeRatesCommand::class, new RateLimiterMiddleware()) ->build();
Custom middleware
Implement MiddlewareInterface:
use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Silencenjoyer\ApiSdk\Handlers\HandlerInterface; use Silencenjoyer\ApiSdk\Middlewares\MiddlewareInterface; class RetryMiddleware implements MiddlewareInterface { public function handle(RequestInterface $request, HandlerInterface $handler): ResponseInterface { $response = $handler->handle($request); if ($response->getStatusCode() === 503) { $response = $handler->handle($request); } return $response; } }
Custom parsers and serializers
Register additional format support on the builder:
use Silencenjoyer\ApiSdk\Constants\Format; NbuApi::getBuilder() ->withAddedParser(Format::JSON, new MyJsonParser()) ->withAddedSerializer('xml', new XmlSerializer()) ->withFormats(Format::JSON, 'xml') ->build();
Serializer resolution
When a command has body params, the SDK selects a serializer using this priority chain:
getRequestContentType()— explicit override on the command.- The
Content-Typeheader already present on the request — set it viagetHeaders()to let the SDK pick the serializer automatically. - If neither provides a recognized content type,
UnableToResolveSerializerExceptionis thrown.
// Option 1 — override on the command protected function getRequestContentType(): ?ContentTypeDto { return new ContentTypeDto(Format::JSON); } // Option 2 — set the header, the SDK reads it protected function getHeaders(): array { return ['Content-Type' => 'application/json']; }
The recognized content types are controlled by withFormats() on the builder.
Parser resolution
After a response is received, the SDK selects a parser using this priority chain:
getAnswerContentType()— explicit override on the command; bypasses all automatic detection.- The
Content-Typeheader on the response — matched against the content types registered viawithFormats(). - Assumes — when the header is absent or unrecognized, the body is inspected by registered assume strategies.
JsonAssumeis active by default. - If none of the above resolves,
UnableToResolveParserExceptionis thrown.
// Force a specific parser regardless of the response header protected function getAnswerContentType(): ?ContentTypeDto { return new ContentTypeDto(Format::JSON); }
Content-type inference
When a response carries no Content-Type header, the SDK uses registered "assumes" to infer the format from the body. JSON is assumed by default. Register additional strategies:
NbuApi::getBuilder() ->withParserContentTypeAssume(new XmlAssume()) ->build();
Built-in components
| Component | Description |
|---|---|
LoggerMiddleware |
Logs request and response bodies via PSR-3 |
RepeatableMiddleware |
Retries requests on 5xx by default; configurable condition and retry count |
RateLimiterMiddleware |
Throttles outgoing requests via silencenjoyer/rate-limiter; blocks until the rate window allows |
Bearer |
Adds Authorization: Bearer <token> header |
JsonParser |
Parses JSON responses |
JsonSerializer |
Serializes request bodies as JSON |
UrlEncodedSerializer |
Serializes request bodies as application/x-www-form-urlencoded |
License
MIT