ldc-arcadia-genesis-php-client/src/Lib/ApiCaller.php
2026-04-16 09:48:49 +02:00

208 lines
6.1 KiB
PHP

<?php
declare(strict_types=1);
namespace Uwil\ArcadiaGenesisPhpClient\Lib;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\ServerException;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Uwil\ArcadiaGenesisPhpClient\Enum\ArcadiaGenesisApiErrorCode;
use Uwil\ArcadiaGenesisPhpClient\Exception\ApiCallFailed;
use Uwil\ArcadiaGenesisPhpClient\Exception\ApiErrorException;
use Uwil\ArcadiaGenesisPhpClient\Exception\InvalidApiResponse;
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiQueryBuilder\TableQuery;
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiResponse\ApiResponse;
use Uwil\ArcadiaGenesisPhpClient\Lib\Psr7\RequestFactory;
use Uwil\ArcadiaGenesisPhpClient\Utils\Utils;
class ApiCaller
{
public function __construct(
private readonly ClientInterface $client,
private readonly ApiConfig $apiConfig,
private readonly RequestFactory $requestFactory
) {
}
public function callApi(string $queryPath, TableQuery $query): ApiResponse
{
$uri = Utils::appendPathTo(
$this->apiConfig->endpoint,
$queryPath
);
$request = $this->requestFactory->buildRequest(
HttpMethod::GET,
$uri,
$query->toArray()
);
return $this->callWithRetry($request);
}
protected function callWithRetry(RequestInterface $request): ApiResponse
{
$maxAttempts = $this->apiConfig->maxRetries + 1;
$lastException = new \RuntimeException("Max attempts '{$maxAttempts}' reached on api call");
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
try {
return $this->call($request);
} catch (ApiCallFailed $e) {
$lastException = $e;
if ($attempt >= $maxAttempts) {
break;
}
$this->delay($attempt);
} catch (ApiErrorException $e) {
if ($e->apiError !== ArcadiaGenesisApiErrorCode::ERR_TOO_MANY_REQUESTS) {
throw $e;
}
$lastException = $e;
if ($attempt >= $maxAttempts) {
break;
}
$this->delay($attempt);
}
}
throw $lastException;
}
protected function call(RequestInterface $request): ApiResponse
{
try {
$response = $this->client->sendRequest($request);
$this->assertResponseHasExpectedHeaders($response);
$body = $this->parseResponseBody($response);
$body = $this->assertApiResponseFormIsAsExpected($body);
if ($this->apiResponseIsError($body)) {
throw ApiErrorException::fromResponseError($body['error']);
}
return ApiResponse::fromBody($body);
} catch (ConnectException $e) {
throw new ApiCallFailed(
'Failed to connect to Api backend: ' . $e->getMessage(),
$e->getCode(),
$e
);
} catch (ClientException $e) {
throw new ApiCallFailed(
'Failed to call Api backend seeing a ClientException',
$e->getCode(),
$e
);
} catch (ServerException $e) {
$response = $e->getResponse();
$body = $this->parseResponseBody($response);
if ($this->apiResponseIsError($body)) {
throw ApiErrorException::fromResponseError($body['error']);
}
throw new ApiErrorException(
ArcadiaGenesisApiErrorCode::UNKNOWN,
$e
);
}
}
/**
* @throws \Uwil\ArcadiaGenesisPhpClient\Exception\InvalidApiResponse
*/
protected function parseResponseBody(ResponseInterface $response): mixed
{
$bodyContent = $response->getBody()->getContents();
if (! json_validate($bodyContent)) {
$jsonErrorMessage = json_last_error_msg();
throw new InvalidApiResponse(
"Invalid Json message '{$jsonErrorMessage}'"
);
}
return json_decode($bodyContent, true);
}
/**
* @throws \Uwil\ArcadiaGenesisPhpClient\Exception\InvalidApiResponse
*/
protected function assertResponseHasExpectedHeaders(ResponseInterface $response): void
{
$headerIsJsonContent = in_array(
$response->getHeaders()['Content-Type'][0] ?? '',
[
'application/json;charset=UTF-8',
'application/json',
],
true
);
if (! $headerIsJsonContent) {
throw new InvalidApiResponse(
'Expected Json header in Content-Type'
);
}
}
/**
* @return array<string,mixed>
*
* @throws \Uwil\ArcadiaGenesisPhpClient\Exception\InvalidApiResponse
*/
protected function assertApiResponseFormIsAsExpected(mixed $body): array
{
$e = new InvalidApiResponse(
'Body content shape cannot be recognized'
);
if (! is_array($body) || ! array_key_exists('success', $body)) {
throw $e;
}
if (! $this->apiResponseIsError($body) && ! $this->apiResponseContainsData($body)) {
throw $e;
}
return $body;
}
/**
* @param array<string, mixed> $responseData
*/
protected function apiResponseIsError(array $responseData): bool
{
return array_key_exists('error', $responseData)
&& is_array($responseData['error'])
&& ! empty($responseData['error']);
}
/**
* @param array<string, mixed> $responseData
*/
protected function apiResponseContainsData(array $responseData): bool
{
return array_key_exists('data', $responseData)
&& is_array($responseData['data']);
}
protected function delay(int $attempt): void
{
$delayMs = $this->apiConfig->retryDelayMs * $attempt;
usleep($delayMs * 1000);
}
}